0x00 Introduction
所有的Java代码最终交给JVM运行时都是需要转换成JVM的字节码,对于每一个类都需要组装成一个合法、完整的Class文件,被JVM载入后才能运行。
Java除了JLS作为语言标准外,还有一份The Java Virtual Machine Specification虚拟机规范,详细描述了Class文件的构成,以及JVM在载入时需要进行的检查、链接过程。这为Sun/Oracle之外的厂商自行实现JVM、编译器提供了可能。
最新的Java8的规范可从下面链接获取:
http://docs.oracle.com/javase/specs/jvms/se8/html/
本文代码是在JDK7 HotSpot下编译的
$ java -version
java version "1.7.0_75"
Java(TM) SE Runtime Environment (build 1.7.0_75-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.75-b04, mixed mode)
您可以从下面链接得到Java7的规范,网上还可以找到这份文件的中文翻译。
http://docs.oracle.com/javase/specs/jvms/se7/html/
作为一个Java码农,一定非常好奇Class的文件结构,同时对字节码的了解也利于性能调优。但是这份冗长的规范文件(如果整理成书将会有接近700页)其实大部分的时间都是在定义各种各样具体的数据结构以及各个指令的作用,如果你不打算亲自实现一个JVM虚拟机,则其中大部分的内容是不需要详细关注的。幸运的是,JDK其实提供了一个官方的反编译工具javap
,用于快速查看Class文件的内容。
本文将提供一个示例类,将帮助您快速了解Class文件的结构,并了解大部分的字节码指令。
0x01 Class
定义一个类,为了方便起见,我把它放在了根包下
import java.util.*;
// A class includes most kinds of JAVA bytecode op
// javac BytecodeExample
// javap -c -v -p BytecodeExample
@Deprecated
public abstract class BytecodeExample<T extends List> {
}
编译后利用javap -c -v -p BytecodeExample
反编译后得到下面的内容
对于javap命令,其中的 -c
参数表示反编译,如果没有该参数则看不到每个方法具体的代码 -v
参数表示verbose输出,会包括本地变量表、调试用的行号等信息 -p
参数表示输出private以上也就是所有的成员
Classfile /*****/target/classes/BytecodeExample.class
Last modified 2016-4-11; size 484 bytes
MD5 checksum d384ebc428f71935665589e7be64fdc3
Compiled from "BytecodeExample.java"
public abstract class BytecodeExample<T extends java.util.List> extends java.lang.Object
Signature: #14 // <T::Ljava/util/List;>Ljava/lang/Object;
SourceFile: "BytecodeExample.java"
Deprecated: true
RuntimeVisibleAnnotations:
0: #19()
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT
Constant pool:
#1 = Methodref #3.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // BytecodeExample
#3 = Class #22 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 LBytecodeExample;
#11 = Utf8 LocalVariableTypeTable
#12 = Utf8 LBytecodeExample<TT;>;
#13 = Utf8 Signature
#14 = Utf8 <T::Ljava/util/List;>Ljava/lang/Object;
#15 = Utf8 SourceFile
#16 = Utf8 BytecodeExample.java
#17 = Utf8 Deprecated
#18 = Utf8 RuntimeVisibleAnnotations
#19 = Utf8 Ljava/lang/Deprecated;
#20 = NameAndType #4:#5 // "<init>":()V
#21 = Utf8 BytecodeExample
#22 = Utf8 java/lang/Object
{
public BytecodeExample();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LBytecodeExample;
LocalVariableTypeTable:
Start Length Slot Name Signature
0 5 0 this LBytecodeExample<TT;>;
}
这里您所看到的内容和原始的Class中文件所存储的内容、顺序是一致的。
对于任意的Class文件,都包括:
- 头部分:包括版本号等。51表示Java7,50表示Java6,以此类推
- 常量池Constant pool。
在整个Class文件中其实都不再有任何常量,包括类名、签名、数字等等,与代码有关的所有常量都在这。另外有一点好玩的是字符串常量可能是另两个常量拼接而成。 - 签名相关。如可访问性,这里是
ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT
。其中ACC_SUPER
是Java1.0.2以后对invokevirtual
命令修改定义后的标识。 - 接口、字段、方法、属性等。属性区是个神奇的地方,包含很多Java新特性的东西,比如@Annotation,比如表示已过时等等。
你要问,Class文件是用怎样的数据结构存储这些信息的?好吧,你适合直接直接看规范文件。
0x02 Hello World
码农的世界从Hello World开始,Java的世界从main函数开始(Stop,别找茬)。
于是我们加入一个打印Hello World的main函数,看看会编译成什么样
// public getstatic invokevirtual
public static void main(String[] args) {
System.out.println("Hello World");
}
javap后可以看到
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello World
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 21: 0
line 22: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
现在我们看到了一个方法被编译之后的样子。
一个方法包括:
- 签名相关。如可访问性和方法签名
- 属性
你可能会好奇,代码放在哪,答案是在属性里。。。
在上面的javap
结果中我们看到了三种属性:
- Code代码
-
LineNumberTable
行号表,是源代码的行号和Code中指令位置直接的映射,用于调试。比如System.out.println("Hello World");
位于21行,被翻译成了从getstatic
到return
之前的三条指令,也就是指令偏移位置的[0:, 8:)
,注意不包括偏移为8个指令,即return
指令。 -
LocalVariableTable
本地变量表,比如上面指出了args变量的名字、本地变量数组的位置、指令作用域[0, 0+9),以及签名。这个也是用于调试
事实上,在Class属性中有很多类似2、3这样的用于调试的东西,而一些运行时生成的类或者非官方的编译器是没有这些信息的。
0x03 Code
在Code部分的一开始,我们看到 stack=2, locals=1, args_size=1
它表示这段代码
-
stack
栈长为2
JVM指令是一种基于栈的指令,所有的指定都是类似于压栈、出栈、对栈顶参数做操作然后再压入栈顶。而stack
就定义了这个栈的长度。 -
locals
本地变量数组长度为1(即args
) - 该方法参数的数量
args_size
为1(即args
)
上面的Hello World示例*包含4条指令
以第一条指令为例
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
0:
表示指令偏移的起点,我们可以看到下一条指令是3:
,这说明第一条指令占了3个字节。
JVM指令和常见的汇编一样,包括操作码和操作数,除了wide开头的指令,其他指令操作码都是一个字节,getstatic
是助记符,实际的二进制数为0xb2
,后面是一个2个字节的操作数,表示常量池位置#3,javap已经给了我们提示,就是java/lang/System.out:Ljava/io/PrintStream
,这表示从java/lang/System
类中获取out
静态
字段,其类型是Ljava/io/PrintStream
,然后推入栈顶。
下面我们人肉跟踪一下这段代码
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
// 获取System.out进入栈顶,现在栈=[System.out]
3: ldc #4 // String Hello World
// 将常数#4(字符串Hello World)推入栈顶,现在栈=[System.out, "Hello World"]
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// 调用PrintStream.println方法,参数为栈顶元素(根据方法签名可以确定有2个参数,第一个是this),返回结果压入栈顶
8: return
// 返回
0x04 Field & Constructor
我们向示例类加入以下代码
// field
public static final int[][][] INT; // 15行
// putfield in generated constructor
private int a = 1; // 17行
// <clinit> putstatic multiarray
static {
INT = new int[1][2][3]; // 136行
}
你会发现javap
后多出如下的东西
1、 字段声明
public static final int[][][] INT;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
private int a;
flags: ACC_PRIVATE
2、 如果没有定义构造函数,会自动生成的构造函数
这个函数会自动调用父类的构造函数,而5:
和6:
这是为了实现对字段a的初始化赋值 load
和const
是很常见的指令。xload_n
表示从第n个本地变量表中载入类型为x的元素压入栈顶(locals
=>stack
),其中x=a
时表示是一个对象。xconst_n
则表示在栈顶压入一个x类型的值为n的量,其中x=i
表示int。 getstatic``putstatic``getfield``putfield
表示栈顶元素(载入、存入) * (静态、类字段)。
public BytecodeExample();
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field a:I
9: return
3、 类构造函数 multianewarray
指令会生成一个多维数组。它有2个操作数,#36=[[[I
表示构造的数组为int[][][]
,而3表示用栈顶3个元素作为每个维度的size。
static {};
stack=3, locals=0, args_size=0
0: iconst_1
1: iconst_2
2: iconst_3
3: multianewarray #36, 3 // class "[[[I"
7: putstatic #37 // Field INT:[[[I
10: return
对于方法部分,我省略了LineNumberTable
等信息,这和javap
命令去掉-v参数的效果是一样的。
详细的指令定义可以从规范文档中得到,相信大部分的指令都是可以不需要查文档即能明白的,对于特殊的命令后面会特别阐述。
0x05 Number
我们向示例类加入以下代码
// this PrimitiveType ConditionalOp return
public int sum(byte b, short s, boolean z) {
return z ? 0 : b + s;
}
ifeq
表示如果等于则跳转到便宜8:
。ireturn
这是返回int型。 StackMapTable
是Java7规范引入的,用于帮助载入类时对Class进行验校,它会表示每个代码段中栈和本地变量表的长度、类型等信息。比如frame_type = 8
表示指令偏移[0:,8:)
直接不需要改变栈和本地变量表的长度。
需要注意的是,无论是byte
还是short
还是boolean
,都是会独立占用一个栈的槽,也就是会被自动转换为32位int存储, 但同时在LocalVariableTable
中标记了他们的类型。 LocalVariableTable
中前n个元素就是这个方法的入参,而且如果不是静态方法,第一个参数就是this。
stack=2, locals=4, args_size=4
0: iload_3
1: ifeq 8
4: iconst_0
5: goto 11
8: iload_1
9: iload_2
10: iadd
11: ireturn
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 this LBytecodeExample;
0 12 1 b B
0 12 2 s S
0 12 3 z Z
StackMapTable: number_of_entries = 2
frame_type = 8 /* same */
frame_type = 66 /* same_locals_1_stack_item */
stack = [ int ]
我们向示例类加入以下代码
// private long box invokestatic
private static Long add(long a, Integer b) {
++b;
long r = a + b;
return r;
}
可以看到代码被编译后都是自动完成拆箱装箱操作的。 i2l
是将int转换为long。
特别注意的是LocalVariableTable
中第一个变量a
,它是一个long类型,占了2个槽,因为JVM规范规定了一个槽就是32位,无视机器是否是64位的。
stack=4, locals=5, args_size=2
0: aload_2
1: invokevirtual #6 // Method java/lang/Integer.intValue:()I
4: iconst_1
5: iadd
6: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
9: astore_2
10: lload_0
11: aload_2
12: invokevirtual #6 // Method java/lang/Integer.intValue:()I
15: i2l
16: ladd
17: lstore_3
18: lload_3
19: invokestatic #8 // Method java/lang/Long.valueOf:(J)Ljava/lang/Long;
22: areturn
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 a J
0 23 2 b Ljava/lang/Integer;
18 5 3 r J
0x06 For Each
我们向示例类加入以下代码
// package_acc foreach arraylength locals varargs
static void forEach(List list, int... arr) {
for (int i : arr) {}
for (Object i : list) {}
}
这是一个包访问的方法,所以没有加上可访问性的标识。
可以看到这两个for each循环都做了隐式的处理:
1. 对于数据实际是转换成了for(int i$=0; i$<arr.length; ++i$)
2. 对于List则转换成了迭代器。
为了实现这点,编译器自动加入了i$等变量,所以可以看到本地变量表locals
的长度并不一定等于代码中声明的变量数量,而且在不同的作用域下,变量可能会复用一个locals
槽,所以locals
的长度可能比声明的变量数量多,也可能少。 if_icmpge
表示对于对于int进行比较,如果>=时跳转,相信这很容易理解。
static void forEach(java.util.List, int...);
flags: ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=6, args_size=2
0: aload_1
1: astore_2
2: aload_2
3: arraylength
4: istore_3
5: iconst_0
6: istore 4
8: iload 4
10: iload_3
11: if_icmpge 26
14: aload_2
15: iload 4
17: iaload
18: istore 5
20: iinc 4, 1
23: goto 8
26: aload_0
27: invokeinterface #9, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
32: astore_2
33: aload_2
34: invokeinterface #10, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
39: ifeq 52
42: aload_2
43: invokeinterface #11, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
48: astore_3
49: goto 33
52: return
LocalVariableTable:
Start Length Slot Name Signature
20 0 5 i I
2 24 2 arr$ [I
5 21 3 len$ I
8 18 4 i$ I
49 0 3 i Ljava/lang/Object;
33 19 2 i$ Ljava/util/Iterator;
0 53 0 list Ljava/util/List;
0 53 1 arr [I
0x07 Invoke
我们向示例类加入以下代码
// native
protected native int nativeFunc(int b);
// abstract
public abstract int hashCode();
// super. intern() invokespecial
void invoke() {
main(new String[0]);
nativeFunc(1);
super.toString().intern();
}
这里有三种invoke,加上之前的invokeinterface
,他们的意义分别是
1. invokestatic
调用静态方法
2. invokeinterface
调用接口声明的方法
3. invokevirtual
调用虚函数方法。JAVA中的方法都是虚函数,final函数主要是对于编译器和类载入检查用的。这也是见得最多的函数调用
4. invokespecial
用于调用构造函数、父函数。
另外,anewarray
用于创建一个一维的数组
protected native int nativeFunc(int);
flags: ACC_PROTECTED, ACC_NATIVE
public abstract int hashCode();
flags: ACC_PUBLIC, ACC_ABSTRACT
void invoke();
flags:
Code:
stack=2, locals=1, args_size=1
0: iconst_0
1: anewarray #12 // class java/lang/String
4: invokestatic #13 // Method main:([Ljava/lang/String;)V
7: aload_0
8: iconst_1
9: invokevirtual #14 // Method nativeFunc:(I)I
12: pop
13: aload_0
14: invokespecial #15 // Method java/lang/Object.toString:()Ljava/lang/String;
17: invokevirtual #16 // Method java/lang/String.intern:()Ljava/lang/String;
20: pop
21: return
0x08 Class Cast
我们向示例类加入以下代码
// new instanceof checkcast
static void tryCast() {
ArrayList arrayList = new ArrayList();
boolean b = arrayList instanceof List;
Object obj = arrayList;
List list = (List) obj;
}
注意0:``3:``4:
共同组成了一个new的操作,其中0:
在栈上创一个空的对象,而4:
对它实施初始化。<init>
是构造函数的方法名,<clinit>
是类构造函数的方法名。
我曾经想过,如果只有new不执行invokespecial
是否就可以实现对任意的类实现无参构造,但是很遗憾,如果没有invokespecial
的话会通不过HotSpot的验校。 instancoef
如果检查成功的话,会压栈1,否则压栈0
而checkcast
在检查失败后直接抛出ClassCastException
(规范中规定了例如cast失败、类载入错误等一堆JVM级的异常)
stack=2, locals=4, args_size=0
0: new #18 // class java/util/ArrayList
3: dup
4: invokespecial #19 // Method java/util/ArrayList."<init>":()V
7: astore_0
8: aload_0
9: instanceof #20 // class java/util/List
12: istore_1
13: aload_0
14: astore_2
15: aload_2
16: checkcast #20 // class java/util/List
19: astore_3
20: return
0x09 Generic Type
在类的定义中您可能已经看到了泛型,方法中的泛型和其大致相同。
我们向示例类加入以下代码
// generic null SignatureOfClass
static <A extends BytecodeExample & List> A generic(List<? super HashSet> set) {
return null;
}
请注意其中的各种方法签名,有时是使用/
,有时则使用.
请注意LocalVariableTypeTable
属性,如果方法中含有与泛型有关的局部变量时,其就会出现在LocalVariableTypeTable
中,这可能是对旧jvm的适配 aconst_null
指令会将null放入栈顶
static <A extends BytecodeExample & java/util/List> A generic(java.util.List<? super java.util.HashSet>);
flags: ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: aconst_null
1: areturn
LocalVariableTypeTable:
Start Length Slot Name Signature
0 2 0 set Ljava/util/List<-Ljava/util/HashSet;>;
Signature: #104 // <A:LBytecodeExample;:Ljava/util/List;>(Ljava/util/List<-Ljava/util/HashSet;>;)TA;
0x0A Final & InnerClass
我们向示例类加入以下代码
// final AnonymousClass invokeinterface
final boolean isFinal(final int a, List<String> list) {
Comparator<String> comparator = new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2) * a;
}
};
Collections.sort(list, comparator);
return true;
}
您可能会发现多了一个BytecodeExample$1
的类,这是编译器自动生成的“匿名类”。
stack=4, locals=4, args_size=3
0: new #21 // class BytecodeExample$1
3: dup
4: aload_0
5: iload_1
6: invokespecial #22 // Method BytecodeExample$1."<init>":(LBytecodeExample;I)V
9: astore_3
10: aload_2
11: aload_3
12: invokestatic #23 // Method java/util/Collections.sort:(Ljava/util/List;Ljava/util/Comparator;)V
15: iconst_1
16: ireturn
而[0:,9:)
的代码也和之间的new
不同,aload_0
和iload_1
分别会把this
和a
压入栈,这说明final参数是通过构造函数传入匿名对象的实例的,同时它还保留了外部类实例的this
引用。
我们可以查看下javap BytecodeExample$1
class BytecodeExample$1 implements java.util.Comparator<java.lang.String> {
final int val$a;
final BytecodeExample this$0;
BytecodeExample$1(BytecodeExample, int);
public int compare(java.lang.String, java.lang.String);
public int compare(java.lang.Object, java.lang.Object);
}
0x0B switch
我们向示例类加入以下代码,您也可以javap BytecodeExample$Color
以了解一个枚举类
// enum InnerClass
enum Color {
RED(1), GREEN(2), BLUE(3);
Color(int a) {}
}
// string_switch lookupswitch tableswitch
static void switchFunc(Color c, String s) {
switch (s) {
case "1": return;
}
switch(c) {
case RED: return;
case GREEN: return;
case BLUE: return;
default: return;
}
}
Java从Java7开始可以对String做switch,在[0:,61:)
中,编译的代码首先求hashCode
然后做equals
以确定是否match。
而[61:,end]
则是采用了tableswitch
指令,tableswitch
和lookupswitch
的区别是tableswitch
用于处理连续的值。事实上,在上面的代码中如果只有2个case,那么我的编译器就会使用lookupswitch
。
stack=2, locals=4, args_size=2
0: aload_1
1: astore_2
2: iconst_m1
3: istore_3
4: aload_2
5: invokevirtual #24 // Method java/lang/String.hashCode:()I
8: lookupswitch { // 1
49: 28
default: 39
}
28: aload_2
29: ldc #25 // String 1
31: invokevirtual #26 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
34: ifeq 39
37: iconst_0
38: istore_3
39: iload_3
40: lookupswitch { // 1
0: 60
default: 61
}
60: return
61: getstatic #27 // Field BytecodeExample$2.$SwitchMap$BytecodeExample$Color:[I
64: aload_0
65: invokevirtual #28 // Method BytecodeExample$Color.ordinal:()I
68: iaload
69: tableswitch { // 1 to 3
1: 96
2: 97
3: 98
default: 99
}
96: return
97: return
98: return
99: return
0x0C Exception
我们向示例类加入以下代码
// exception
static void exception() throws RuntimeException {
try {
throw new NullPointerException();
} catch (RuntimeException e) {
e.printStackTrace();
} catch (Error | Exception e) {
throw e;
} finally {
return;
}
}
Exception table
展示了catch部分的实现,会依次罗列异常处理器。[from, to)
表示了字节码偏移量的区间,也就是try{}部分的代码。 Exception table
展示了finally部分的处理,finally会作为any的异常处理器出现。
Java7支持用|
来声明一组异常,在编译器中被自动调整为了他们共同的父类Throwable
,所以LocalVariableTable
的第二个本地变量的类型是Throwable
。 athrow
指令抛出栈顶异常。
static void exception() throws java.lang.RuntimeException;
flags: ACC_STATIC
Code:
stack=2, locals=2, args_size=0
0: new #30 // class java/lang/NullPointerException
3: dup
4: invokespecial #31 // Method java/lang/NullPointerException."<init>":()V
7: athrow
8: astore_0
9: aload_0
10: invokevirtual #33 // Method java/lang/RuntimeException.printStackTrace:()V
13: return
14: astore_0
15: aload_0
16: athrow
17: astore_1
18: return
Exception table:
from to target type
0 8 8 Class java/lang/RuntimeException
0 8 14 Class java/lang/Error
0 8 14 Class java/lang/Exception
0 13 17 any
14 18 17 any
LocalVariableTable:
Start Length Slot Name Signature
9 4 0 e Ljava/lang/RuntimeException;
15 2 0 e Ljava/lang/Throwable;
Exceptions:
throws java.lang.RuntimeException
0x0D Synchronized
我们向示例类加入以下代码
// synchronized monitorenter/monitorexit
static synchronized void synchronizedFunc() {
Object o = new Object();
synchronized (o) {
}
}
虽然您可能了解到synchronized
关键字无论是作用在方法上还是代码块上,其在JVM中的处理并没有很大的不同,但是在Class文件的规范定义中,是完全不同的。 synchronized
作用于方法时是以签名的形式存在,而对代码块则是monitorenter/monitorexit
。
特别注意的是,synchronized
代码块隐式加上了finally块,用于防止有异常时没有释放锁。
static synchronized void synchronizedFunc();
flags: ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=3, args_size=0
0: new #29 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_0
8: aload_0
9: dup
10: astore_1
11: monitorenter
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
12 14 17 any
17 20 17 any
0x0E Annotation
我们向示例类加入以下代码
@Deprecated
@Nullable
static void annotation(@Nullable int resource) {
}
javap
反编译的结果如下。这里要关注的就是RuntimeVisibleAnnotations
、RuntimeInvisibleAnnotations
和RuntimeInvisibleParameterAnnotations
三个属性。@Nullable注解的RetentionPolicy
并没有Runtime
,所以是RuntimeInvisibleAnnotations
的。
static void annotation(int);
flags: ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 110: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 resource I
Deprecated: true
RuntimeVisibleAnnotations:
0: #126()
RuntimeInvisibleAnnotations:
0: #128()
RuntimeInvisibleParameterAnnotations:
0:
0: #128()
0x0F try-finally
JAVA中有一个经典的问题,就是try-finally中的多return问题
static int multiReturn() {
try {
return 1;
} finally {
return 2;
}
}
javap
如下,你知道应该返回什么吗?
static int multiReturn();
flags: ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: iconst_1
1: istore_0
2: iconst_2
3: ireturn
4: astore_1
5: iconst_2
6: ireturn
Exception table:
from to target type
0 2 4 any
4 5 4 any
完整的代码请戳
https://github.com/zillionbrains/exercise/blob/master/bytecode_example/BytecodeExample
如果您有编译问题,请尝试移除108、109行代码的 @Nullable