JDK-6354728 : Verification of signed JAR files is very slow (performance reduction)
  • Type: Bug
  • Component: security-libs
  • Sub-Component: java.security
  • Affected Version: 1.4.2,5.0,5.0u5,5.0u6
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: solaris_10,windows_xp
  • CPU: x86,sparc
  • Submitted: 2005-11-23
  • Updated: 2010-05-11
  • Resolved: 2005-12-22
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
Other JDK 6
1.4.2_13Fixed 6Fixed
Related Reports
Duplicate :  
Duplicate :  
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
java version "1.4.2_10"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2_10-b03)
Java HotSpot(TM) Client VM (build 1.4.2_10-b03, mixed mode)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows XP [Version 5.1.2600]

A DESCRIPTION OF THE PROBLEM :
When any of the following JRE releases:

Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2_10-b03)
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_04-b05)
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_05-b05)

The time it takes for Sun to verify a signed JAR file increases significantly.  This issue specifically effects any Java products that contain a JCE/JCE cryptographic provider, which is signed (and must be signed).  The performance reduction is directly related to the number of entries in the JAR file and the number of entries in the manifest.  For example, below is the slow-down observed when accessing an AES cipher from a third-party JCE/JCA provider that is shipped in a signed JAR file of various sizes:

Case #1
------------
JAR file size: 800KB
# JAR entries: 400
Performance Impact: 2x slowdown (1.3s versus 2.8s)

Case #2
-------------
JAR file size: 3740KB
# JAR entries: 2465
Performance Impact: 20x slowdown (3.1s versus 56.7s)

The performance comparison was done using JRE 1.5.0_03(fast) and JRE 1.5.0_05 (extremely slow).  The larger the signed JAR file, the worse the performance penalty.  A start-up performance penalty of over 1 minute is simply not acceptable.

The problem results from a change that was made to the JAR classes; this change is also described in bug 6349606.  It appears that a change was made in JarFile.getInputStream() such that a call to this method results in call to getManifest(). When using the JarFile class, no performance degradation is seen because JarFile.java has a manifest caching mechanism.  However, when using sun.net.www.protocol.jar.URLJarFile, the getManifest() call performs a deep copy of the manifest and does not have a caching mechanism (or at least one that works).  This means that when each entry of the JAR file  is read, the entire Manifest is copied (which means copying every entry in the Manifest)... this has a disastrous impact on performance.

Sun's JAR verification mechanism uses URLJarFile, so this impacts the performance of JAR verification.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
The following program is the simplest demonstration of the problem; it accepts one argument which is the path of any signed jar file.  It then attempts to read every entry in the JAR file using JarFile (causing the JAR file to be verified), then does the same thing using URLJarFile.

import java.io.File;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

public class DemoSlowJarRead {

    /**
     * @param args
     */
    public static void main(String[] args) throws Exception {
        if (args.length != 1)
            throw new IllegalArgumentException("Usage: DemoURLJarFileRead <path to signed JAR file> ");
        File file = new File(args[0]);
        JarFile jarFile = new JarFile(file);
        System.out.println("Reading JAR file from File: \n  " + file);
        readJarFile(jarFile);

        // Read JAR from URL
        URL url = new URL("jar:file:/" + file.getAbsolutePath() + "!/");
        JarURLConnection conn = (JarURLConnection) url.openConnection();
        conn.setUseCaches(false);
        jarFile = conn.getJarFile();
        System.out.println("Reading JAR file from URL: \n  " + url);
        readJarFile(jarFile);
    }

    private static void readJarFile(JarFile jarFile) throws Exception {
        long l = System.currentTimeMillis();

        Vector entriesVec = new Vector();

        // Ensure the jar file is signed.
        Manifest man = jarFile.getManifest();
        if (man == null) {
            throw new SecurityException("The jar file is not signed");
        }

        byte[] buffer = new byte[8192];
        int i = 0;
        Enumeration entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            i++;

            JarEntry je = (JarEntry) entries.nextElement();

            // Skip directories.
            if (je.isDirectory())
                continue;

            entriesVec.addElement(je);
            InputStream is = jarFile.getInputStream(je);

            // Read in each jar entry. A security exception will
            // be thrown if a signature/digest check fails.
            while ((is.read(buffer, 0, buffer.length)) != -1) {
                // Don't care
            }
            is.close();
        }
        l = System.currentTimeMillis() - l;
        System.out.println("\nManifest Entries: " + man.getEntries().size());
        System.out.println("JAR File Entries: " + i);
        System.out.println("Took: " + l / 1000.0 + "s\n\n");
    }
}


EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The expected result is that both the JarFile and URLJarFile method of reading the JAR file should have near identical performance.  The actual result is much different.

These results were produced using JRE 1.5.0_03

RESULT 1 (800KB JAR File)
------------------------------------------
Reading JAR file from File:
  D:\JTK\7.1 SP1\patch\100821\etjava\lib\entbase.jar

Manifest Entries: 439
JAR File Entries: 490
  Took: 0.532s


Reading JAR file from URL:
  jar:file:/D:\JTK\7.1 SP1\patch\100821\etjava\lib\entbase.jar!/

Manifest Entries: 439
JAR File Entries: 490
  Took: 0.328s

RESULT 2 (3740KB JAR File)
------------------------------------------
Reading JAR file from File:
  D:\JTK\7.1 SP1\patch\100821\etjava\lib\enttoolkit.jar

Manifest Entries: 2465
JAR File Entries: 2658
  Took: 1.578s


Reading JAR file from URL:
  jar:file:/D:\JTK\7.1 SP1\patch\100821\etjava\lib\enttoolkit.jar!/

Manifest Entries: 2465
JAR File Entries: 2658
  Took: 1.047s
ACTUAL -
These results were produced using JRE 1.5.0_05

RESULT 1 (800KB JAR File)
------------------------------------------
Reading JAR file from File:
  D:\JTK\7.1 SP1\patch\100821\etjava\lib\entbase.jar

Manifest Entries: 439
JAR File Entries: 490
  Took: 0.5s


Reading JAR file from URL:
  jar:file:/D:\JTK\7.1 SP1\patch\100821\etjava\lib\entbase.jar!/

Manifest Entries: 439
JAR File Entries: 490
  Took: 0.843s


RESULT 2 (3740KB JAR File)
------------------------------------------
Reading JAR file from File:
  D:\JTK\7.1 SP1\patch\100821\etjava\lib\enttoolkit.jar

Manifest Entries: 2465
JAR File Entries: 2658
  Took: 1.547s


Reading JAR file from URL:
  jar:file:/D:\JTK\7.1 SP1\patch\100821\etjava\lib\enttoolkit.jar!/

Manifest Entries: 2465
JAR File Entries: 2658
  Took: 32.703s




REPRODUCIBILITY :
This bug can be reproduced always.

Comments
EVALUATION Yes, this is a bug and the fix is relatively trivial.
30-11-2005

EVALUATION One of those "cross-category" bugs. It is not clear whether it belongs to classes_util_jarzip, classes_security, or classes_net
23-11-2005