问题
When compiling the following code with a simple try/finally
block, the Java Compiler produces the output below (viewed in the ASM Bytecode Viewer):
Code:
try
{
System.out.println("Attempting to divide by zero...");
System.out.println(1 / 0);
}
finally
{
System.out.println("Finally...");
}
Bytecode:
TRYCATCHBLOCK L0 L1 L1
L0
LINENUMBER 10 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Attempting to divide by zero..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 11 L2
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_1
ICONST_0
IDIV
INVOKEVIRTUAL java/io/PrintStream.println (I)V
L3
LINENUMBER 12 L3
GOTO L4
L1
LINENUMBER 14 L1
FRAME SAME1 java/lang/Throwable
ASTORE 1
L5
LINENUMBER 15 L5
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L6
LINENUMBER 16 L6
ALOAD 1
ATHROW
L4
LINENUMBER 15 L4
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L7
LINENUMBER 17 L7
RETURN
L8
LOCALVARIABLE args [Ljava/lang/String; L0 L8 0
MAXSTACK = 3
MAXLOCALS = 2
When adding a catch
block in between, I noticed that the Compiler copied the finally
block 3 times (not posting the bytecode again). This seems like a waste of space in the class file. The copying also doesn't seem to be limited to a maximum number of instructions (similar to how inlining works), since it even duplicated the finally
block when I added more calls to System.out.println
.
However, the result of a custom compiler of mine that uses a different approach of compiling the same code works exactly the same when executed, but requires less space by using the GOTO
instruction:
public static main([Ljava/lang/String;)V
// parameter args
TRYCATCHBLOCK L0 L1 L1
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Attempting to divide by zero..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ICONST_1
ICONST_0
IDIV
INVOKEVIRTUAL java/io/PrintStream.println (I)V
GOTO L2
L1
FRAME SAME1 java/lang/Throwable
POP
L2
FRAME SAME
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "Finally..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L3
RETURN
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
MAXSTACK = 3
MAXLOCALS = 1
Why does the Java Compiler (or the Eclipse Compiler) copy the bytecode of the finally
block multiple times, even using athrow
to rethrow exceptions, when the same semantics can be achieved using goto
? Is this part of the optimization process, or is my compiler doing it wrong?
(The output in both cases is...)
Attempting to divide by zero...
Finally...
回答1:
Inlining Finally Blocks
The question your asking has been analyzed in part at http://devblog.guidewire.com/2009/10/22/compiling-trycatchfinally-on-the-jvm/ (wayback machine web archive link)
The post will show an interesting example as well as information such as (quote):
finally blocks are implemented by inlining the finally code at all possible exits from the try or associated catch blocks, wrapping the whole thing in essentially a “catch(Throwable)” block that rethrows the exception when it finishes, and then adjusting the exception table such that the catch clauses skip over the inlined finally statements. Huh? (Small caveat: prior to the 1.6 compilers, apparently, finally statements used sub-routines instead of full-on code inlining. But we’re only concerned with 1.6 at this point, so that’s what this applies to).
The JSR instruction and Inlined Finally
There are differing opinions as to why inlining is used though I have not yet found a definitive one from an official document or source.
There are the following 3 explanations:
No offer advantages - more trouble:
Some believe that finally in-lining is used because JSR/RET did not offer major advantages such as the quote from What Java compilers use the jsr instruction, and what for?
The JSR/RET mechanism was originally used to implement finally blocks. However, they decided that the code size savings weren't worth the extra complexity and it got gradually phased out.
Problems with verification using stack map tables:
Another possible explanation has been proposed in the comments by @jeffrey-bosboom, who I quote below:
javac used to use jsr (jump subroutine) to only write finally code once, but there were some problems related to the new verification using stack map tables. I assume they went back to cloning the code just because it was the easiest thing to do.
Having to Maintain Subroutine Dirty Bits:
An interesting exchange in the comments of question What Java compilers use the jsr instruction, and what for? points that JSR and subroutines "added extra complexity from having to maintain a stack of dirty bits for the local variables".
Below the exchange:
@paj28: Would the jsr have posed such difficulties if it could only call declared "subroutines", each of which could only be entered at the start, would only be callable from one other subroutine, and could only exit via ret or abrupt completion (return or throw)? Duplicating code in finally blocks seems really ugly, especially since finally-related cleanup may often invoke nested try blocks. – supercat Jan 28 '14 at 23:18
@supercat, Most of that is already true. Subroutines can only be entered from the start, can only return from one place, and can only be called from within a single subroutine. The complexity comes from the fact that you have to maintain a stack of dirty bits for the local variables and when returning, you have to do a three-way merge. – Antimony Jan 28 '14 at 23:40
回答2:
Compiling this:
public static void main(String... args){
try
{
System.out.println("Attempting to divide by zero...");
System.out.println(1 / 0);
}catch(Exception e){
System.out.println("Exception!");
}
finally
{
System.out.println("Finally...");
}
}
And looking at result of javap -v, the finally block is simply appended at the end of every section that manages an exception (adding the catch, a finally block at line 37 is added, the one at 49 is for unchecked java.lang.Errors):
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=3, locals=3, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Attempting to divide by zero...
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: iconst_1
12: iconst_0
13: idiv
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #6 // String Finally...
22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: goto 59
28: astore_1
29: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
32: ldc #8 // String Exception!
34: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
40: ldc #6 // String Finally...
42: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: goto 59
48: astore_2
49: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
52: ldc #6 // String Finally...
54: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
57: aload_2
58: athrow
59: return
Exception table:
from to target type
0 17 28 Class java/lang/Exception
0 17 48 any
28 37 48 any
Looks like that the original finally blocks implementation resembled what you are proposing but since Java 1.4.2 javac started inlining finally blocks, from "An Evaluation of Current Java Bytecode Decompilers"[2009] of Hamilton & Danicic:
Many of the old decompilers expect the use of subroutines for try-finally blocks but javac 1.4.2+ generates inline code instead.
A blog post from 2006 that discusses this:
The code in lines 5-12 is identical to the code in lines 19-26, which actually translates to the count++ line. The finally block is clearly copied.
来源:https://stackoverflow.com/questions/29061627/why-does-the-java-compiler-copy-finally-blocks