One Reason Unused Classes May Get Loaded
By Adrian Sutton
In general, Java classes are lazily loaded into the system, so if you have something like1:
if (System.getProperty("java.version").equals("1.4")) { Java14OnlyClass.doStuff(); } else { AlwaysAvailableAlternative.doStuff(); }
The code will have to be compiled on Java 1.4, but will actually run on Java 1.3 without throwing a ClassNotFoundException. This trick however can be dangerous, there isn’t any guarantee that class loaders will always use lazy loading and even with current class loaders, there are occasionally some surprises.
For instance, if we take that same code and add a try catch statement:
if (System.getProperty("java.version").equals("1.4")) { try { Java14OnlyClass.doStuff(); } catch (Exception e) { e.printStackTrace(); } } else { AlwaysAvailableAlternative.doStuff(); }
whenever the enclosing class is loaded on Java 1.3, it will throw a ClassNotFoundException claiming that Java14OnlyClass could not be found, even though that code is never run. The reason is that the class loader will automatically load any classes referenced in a try/catch block instead of using lazy loading.
Somewhat surprisingly though, this search for references doesn’t descend into method calls. So to make it work with the exception handling, we could use something like:
private void doStuff() { if (System.getProperty("java.version").equals("1.4")) { try { doJava14Stuff(); } catch (Exception e) { e.printStackTrace(); } } else { AlwaysAvailableAlternative.doStuff(); } } private void doJava14Stuff() throws Exception { Java14OnlyClass.doStuff(); }
Useful to know if you suddenly start seeing ClassNotFoundExceptions for class references that are surrounded by a specific version check like this. My best guess is that the VM optimizes the exception handling code somehow which makes the class get loaded immediately. Why it would do so or exactly what it’s doing goes beyond my understanding of the VM’s internals.
One other surprising case I’ve run into, calling setPreferredSize on a component caused unused classes to be loaded which triggered a ClassCastException. I ran into that a number of years ago so my memory of the exact details are somewhat sketchy, but I’m quite confident that it was a call to setPreferredSize that triggered all the classes referenced by the current class to be loaded immediately. It couldn’t have been because setPreferredSize triggered a layout and the knock-on method calls triggered a reference to the class because there’s no way I could have created an instance of it and added it to the layout.
In general, a much better approach is to define an interface that is always available and load the required implementation via Class.forName, that way there is no reference to the class in bytecode at all and nothing the classloader can do will cause it to be loaded ahead of time, saving an awful lot of hassle.
1 – Ignoring the fact that this is a really brittle check for Java 1.4. ↩