JDK-6831731 : URLClassLoader runs slower if given jar:file:/x.jar!/ than if given file:/x.jar
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.net
  • Affected Version: 5.0
  • Priority: P4
  • Status: Open
  • Resolution: Unresolved
  • OS: linux
  • CPU: x86
  • Submitted: 2009-04-17
  • Updated: 2022-01-07
Related Reports
Relates :  
Description
---%<---
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class URLClassPathBug {
    public static void main(String[] args) throws Exception {
//        System.setSecurityManager(new SecurityManager() {
//            public @Override void checkPermission(Permission perm) {}
//        });
        File j = new File(System.getProperty("java.home"), "lib/rt.jar");
        assert j.isFile() : j;
        URL base = j.toURI().toURL();
        URL incorrect = base;
        URL correct = new URL("jar:" + base + "!/");
        for (URL u : new URL[] {incorrect, correct}) {
            System.err.println("Checking: " + u);
            List<Long> times = new ArrayList<Long>();
            for (int i = 0; i < 20; i++) {
                times.add(time(u));
            }
            Collections.sort(times);
            System.err.printf("  median time: %5dusec\n", times.get(times.size() / 2));
            debug(u);
        }
    }
    private static long time(URL u) throws Exception {
        ClassLoader l = new URLClassLoader(new URL[] {u});
        System.gc();
        long start = System.nanoTime();
        for (int i = 0; i < 10; i++) {
            l.getResource("nonexistent");
        }
        long end = System.nanoTime();
        long usec = (end - start) / 1000;
        System.err.printf("  %5dusec\n", usec);
        return usec;
    }
    private static void debug(URL u) throws Exception {
        ClassLoader l = new URLClassLoader(new URL[] {u});
        l.getResource("nonexistent");
        Field f = URLClassLoader.class.getDeclaredField("ucp");
        f.setAccessible(true);
        Object ucp = f.get(l);
        f = ucp.getClass().getDeclaredField("loaders");
        f.setAccessible(true);
        System.err.println("  l.ucp.loaders=" + f.get(ucp));
    }
}
---%<---

JDK 6 produces in a typical run:

---%<---
Checking: file:/space/jdk1.6.0_11/jre/lib/rt.jar
   7823¿sec
   3049¿sec
   2404¿sec
  17242¿sec
   1820¿sec
   2815¿sec
   6870¿sec
   1677¿sec
  12400¿sec
   1549¿sec
   1604¿sec
   1667¿sec
   1559¿sec
   1536¿sec
   1522¿sec
   1534¿sec
   1548¿sec
   1587¿sec
   1585¿sec
   1589¿sec
  median time:  1667¿sec
  l.ucp.loaders=[sun.misc.URLClassPath$JarLoader@1833955]
Checking: jar:file:/space/jdk1.6.0_11/jre/lib/rt.jar!/
   5187¿sec
   3655¿sec
   3775¿sec
   3512¿sec
   3365¿sec
   3178¿sec
   3073¿sec
   3251¿sec
   3402¿sec
   2852¿sec
   2682¿sec
   2721¿sec
   2786¿sec
   2753¿sec
   2691¿sec
   2660¿sec
   2680¿sec
   2676¿sec
   2870¿sec
   2614¿sec
  median time:  2870¿sec
  l.ucp.loaders=[sun.misc.URLClassPath$Loader@b66cc]
---%<---

If you treat 'jar'-protocol URLs consistently with all other URLs - which is to say, consider 'u' to be a classpath element if 'new URL(u, r)' is the correct way to refer to a classpath resource 'r' - then you should use 'jar:file:/x.jar!/' as a classpath entry representing the root directory entry of the file '/x.jar'. Indeed URLClassLoader accepts this usage in its constructor. (It always _returns_ the 'jar'-protocol URLs from calls to getResource.)

Unfortunately, there is a bug in URLClassPath which causes the correct URLs to be searched more slowly than others. URLClassPath.getLoader(URL) does not even check for the 'jar' protocol, falling back to the generic Loader implementation rather than the specialized JarLoader implementation. Loader.findResource uses an inefficient search mechanism, based on actually opening a URLConnection and treating an IOException as "missing".

URLCP.Loader also calls check(URL) whether the resource can be found or not, which in the presence of a SecurityManager forces creation of a FilePermission, which is relatively slow due to the need to canonicalize the file path.
Apparently Bugster is not Unicode-compliant; the special char in the source was intended to be GREEK SMALL LETTER MU.

Comments
SUGGESTED FIX getLoader(URL) should check for 'jar'-protocol URLs (ending in "!/", i.e. corresponding to root JAR entry and not a subdir). If found, just create a JarLoader as normal, after stripping off the leading "jar:" and the trailing "!/".
17-04-2009

PUBLIC COMMENTS Cause of http://www.netbeans.org/nonav/issues/show_bug.cgi?id=162158
17-04-2009

WORK AROUND Only pass "bare" JAR files to the URLClassLoader constructor.
17-04-2009