Here's the bytecode for the deadlocked example:
public class Main extends java.lang.Object{
public static final java.lang.Object a;
public Main();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: return
static {};
Code:
0: new #2; //class java/lang/Object
3: dup
4: invokespecial #1; //Method java/lang/Object."<init>":()V
7: putstatic #3; //Field a:Ljava/lang/Object;
10: invokestatic #4; //Method java/lang/Runtime.getRuntime:()Ljava/lang/Runtime;
13: new #5; //class Main$1
16: dup
17: invokespecial #6; //Method Main$1."<init>":()V
20: invokevirtual #7; //Method java/lang/Runtime.addShutdownHook:(Ljava/lang/Thread;)V
23: iconst_0
24: invokestatic #8; //Method java/lang/System.exit:(I)V
27: return
}
And here's the bytecode for the case that finishes normally:
public class Main extends java.lang.Object{
public static final java.lang.Object a;
public Main();
Code:
0: aload_0
1: invokespecial #1; //Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: return
static {};
Code:
0: new #2; //class java/lang/Object
3: dup
4: invokespecial #1; //Method java/lang/Object."<init>":()V
7: putstatic #3; //Field a:Ljava/lang/Object;
10: getstatic #3; //Field a:Ljava/lang/Object;
13: astore_0
14: invokestatic #4; //Method java/lang/Runtime.getRuntime:()Ljava/lang/Runtime;
17: new #5; //class Main$1
20: dup
21: aload_0
22: invokespecial #6; //Method Main$1."<init>":(Ljava/lang/Object;)V
25: invokevirtual #7; //Method java/lang/Runtime.addShutdownHook:(Ljava/lang/Thread;)V
28: iconst_0
29: invokestatic #8; //Method java/lang/System.exit:(I)V
32: return
}
The bytecode is obviously different. I'll either come up with the answer or someone else who understands the internals of the JVM will help.