Monday, July 09, 2007

Java Puzzler

I've been reading Java Puzzlers by Josh Bloch and Neal Gafter. I usually read (and hopefully solve) one puzzle each day. I had one last week that was ridiculously hard. It was puzzle #44 Cutting Class. The puzzle is pretty simple. You have a simple class called Missing. Then you have two other classes, Strange1 and Strange2. Both have a main method where they construct a new instance of Missing using the usual

Missing m = new Missing();

Both classes also have a try ... catch block where they catch a NoClassDefFoundError. The difference in the two classes is that the first (Strange1) wraps all references to Missing inside the try, but the other(Strange2) does a blank initialization (Missing m;) outside, and then invokes the constructor inside (m = new Missing()) the try.

In the puzzle, you compile all three class and then delete Missing.class. You then execute Strange1 and Strange2. The puzzle is that Strange1 throws a NoClassDefFoundError, but Strange2 does not.

In the solution, the authors point out that the behavior is undefined, so it is possible for different JVMs to exhibit different behavior. The book was written with Java 1.5 in mind. I tried a couple of different 1.5 JVMs (HotSpot and IBM's) and both behaved just as the book claimed.

In the solution to the problem, you have to use a little known part of the JDK, the javap tool. I had not used javap in a long, long time. I used it once to track down a problem that turned out to be a classloader issue. Anyways, javap shows you the instructions that the JVM is going to execute. The key to the puzzler is that in Strange1, the memory location allocated for the instance of Missing has to get merged with the instance of NoClassDefFoundError caught in the catch block. This causes the JVM to have to computer the most specific common superclass of the two classes, and thus load Missing. This causes a NoClassDefFoundError to be thrown before any execution can take place. In Strange2, the memory location is allocated outside the try ... catch, so it is different location than the location for the caught NoClassDefFoundError, so no merging, etc.

For kicks, I then tried a couple of 1.6 JVMs( HotSpot and JRockit.) With a 1.6 JVM, neither Strange1 nor Strange2 throw a NoClassDefFoundError! I did a javap on the 1.6 class files, and they were identical to the 1.5 class files. So what gives?

The key is JSR 202. This JSR introduced a new class verifier (the split verifier) in Java 1.6. One of the ideas on this JSR was to improve class loading time by speeding up class verification. Class verification is where the exception gets thrown in the puzzler. So how does the new class verifier avoid the exception? Basically it allows for extra metadata to be included in the class file around join points. The merging of the Missing instance and the NoClassDefFoundError instance is a join point. So with Java 1.6, the Missing class does not have to be loaded to compute the most specific superclass of the type merged variables because it is already embedded in the class file. So the Missing instance is not attempted to be loaded until its constructor is invoked during execution, and at that point the error is caught. Beautiful.

3 comments:

Neal Gafter said...

Nice detective work!

Anonymous said...

Let me add my congratulations. We will mention this on the books's web site (when I get around to updating it).

Unknown said...

Thanks for the compliments! I would love to hear either of your opinions on the danger of auto unboxing: http://fupeg.blogspot.com/2007/07/auto-unboxing-in-java.html .