This is possibly related to the way try/finally is contemporarily turned into bytecode (as in, since.. JDK... 6? It's been a long long time, but in the distant past, opcodes JMP and RET were used for this; those opcodes are no longer emitted by javac
and haven't been for a very very long time).
The finally block is just repeated. Loads of times, if needed. java translates it to, basically, 'catch anything, run the finally block, then rethrow whatever you caught', + '... and duplicate this code at the end of the body of the try block', + '... and duplicate this code at the end of each and every catch block'.
Thus, in your example code, your finally
block is actually present in your compiled bytecode 3 times. Your code is syntax desugared first from:
try {
f1();
}
catch (InterruptedException e) {
f2();
throw e;
}
finally {
f3(); //breakpoint hit twice
}
to:
try {
f1();
f3(); // CAVEAT 1
}
catch (InterruptedException e) {
try {
f2();
f3(); // CAVEAT 2
} catch (Throwable t) {
f3();
throw t;
}
}
catch (Throwable t) {
f3();
throw t;
}
CAVEAT1: Though, with additional bookkeeping that f3()
isn't re-invoked in response to throwing an exception itself. In java code that's hard to easily write, in bytecode its trivial - try/catch blocks work by declaring 'this range of opcodes? Jump to this code if exceptions occur'. The f3()
duplication in the try block simply isn't part of the range.
CAVEAT2: The bytecode generated is efficient enough to only have the finally body once per catch block.
Hence, you have many different bytecode in your class file that all has the same 'line number'. Debugging fundamentally occurs in bytecode (e.g. when you set a breakpoint, you pick a line. However, some system needs to translate that line to an actual bytecode, and breakpoint that bytecode. In this case, that's tricky: The one line you breakpoint actually has 3 completely different bytecode items. Presumably, your debugger adds breakpoint hooks to all of them.
Just guessing, but it's easy to say how this leads to accidental double firing. It shouldn't - your debugger is clearly itself buggy (heh), but if you want to figure out why its happening, or even help out and write a PR to fix the problem, this is where I'd start.
You can check this 'whaaaa? Are finally
blocks duplicated this much? Crazy!' stuff by using javap -c
, which shows you bytecode.
For example:
class Test { void foo() {
try {
System.out.println("A");
} catch (NullPointerException e) {
System.out.println("B");
} finally {
System.out.println("C");
}
}}
Then javac Test.java; javap -c Test
shows:
void foo();
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String A
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #21 // String C
13: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: goto 50
19: astore_1
20: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
23: ldc #25 // String B
25: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #21 // String C
33: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: goto 50
39: astore_2
40: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
43: ldc #21 // String C
45: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
48: aload_2
49: athrow
50: return
Exception table:
from to target type
0 8 19 Class java/lang/NullPointerException
0 8 39 any
19 28 39 any
breaking that down: You can easily see how C
is loaded three times, but A
and B
only once, which is bizarre given that C is only mentioned once in the source code - until you realize that finally
blocks are duplicated all over the place.