从字节码角度看java代码
接下来,我们要做的事情是尽可能的熟悉字节码指令,这会让我们对java代码的理解会有本质上的飞跃。
1. 字节码指令集
1.1 指令集合
事实上我们接下去的学习都是围绕着字节码指令,字节码指令可以被分割成很多个具有类似含义的组别,之后的学习如果追求速度,基本上可以组别单独拿一个指令出来玩一玩。在这里我们把所有的指令都展示一下




















我们先分析一把这些指令组别,我们知道,代码执行最小单元是帧栈,帧栈里面有三个部分的数据在不断骚动:
- 指向运行时常量池的引用
- 局部变量表
- 操作数栈
那么意味着必须有一些指令处理,这三个之间的数据流转。
- 栈顶数据保存到局部变量表:xstrore类指令,x根据具体数据类型会发生变化
- 局部变量表将数据保存造栈顶:xload类指令,x根据具体数据类型会发生变化
- 常量引入栈顶(不需要常量池):xconst类指令,x根据具体数据类型发生变化
- 常量池数据引入栈顶:idc类指令
- 栈顶本身的一些操作:pop,dup,swap指令
有以上的组别指令就可以解决三个位置上的数据的迁移操作了。
那么对应我们平时的代码书写又有以下的分类
-
对数据的操作有好几个小分支:
- 浮点运算(包括float与double的加减乘除,取负值,取模运算)
- 整数运算(包括int与long的加减乘除,取负值,取模运算)
- 逻辑运算(包括对两个数的按位与或非操作,按位对数字的移位操作)
- 数字类型转换
-
数组操作相关
-
流程控制(包含判断指令,跳转指令,对应的就可以组成if,for,while,switch的流程控制语句)
-
对象操作指令
-
方法调用指令
-
方法返回指令
-
异常相关指令(可以并入流程控制相关)
-
同步相关
简单又强大的指令集,简简单单的百来个指令就盖出了java这座大楼!
2. 从字节码角度看java语句背后的原理
既然我们希望换一个角度看看java语言,那么我们就按照java知识脉络的走势进行描述。
任何一本java入门书,知识的传递必然是循序渐进的。下面把一些主干的知识点拎出来(不完整的,只是为了后面的字节码对应上)
- 基本数据类型
- 算数运算与逻辑运算符
- 流程控制
- 面向对象编程
- 类初始化
- 类方法调用
- 异常处理机制
- 集合框架与泛型
- lambda表达式与流式处理api
- io
- 多线程
- 反射
那么我们在理解字节码的时候,也可以按照这样的顺序进行一一对应
2.1 指令基础
在这部分,将演示大部分的指令的使用
2.1.1 加载与存储指令
加载(load)和存储(store)相关的指令是使用得最频繁的指令,分为load类、store类、常量加载这三种。
- load类指令是将局部变量表中的变量加载到操作数栈,比如iload_0将局部变量表中下标为0的int型变量加载到操作数栈上,根据不同的数据变量类型还有lload、fload、dload、aload这些指令,分别表示加载局部变量表中long、float、double、引用类型的变量。
- store类指令是将栈顶的数据存储到局部变量表中,比如istore_0将操作数栈顶的元素存储到局部变量表中下标为0的位置,这个位置的元素类型为int,根据不同的数据变量类型还有lstore、fstore、dstore、astore这些指令。
- 常量加载相关的指令,常见的有const类、push类、ldc类。const、push类指令是将常量值直接加载到操作数栈顶,比如iconst_0是将整数0加载到操作数栈上,bipush 100是将int型常量100加载到操作数栈上。ldc指令是从常量池加载对应的常量到操作数栈顶,比如ldc#10是将常量池中下标为10的常量数据加载到操作数栈上。
为什么同是int型常量,加载需要分这么多类型呢?这是为了使字节码更加紧凑,int型常量值根据值n的范围,使用的指令按照如下的规则。
- 若n在[-1,5]范围内,使用iconst_n的方式,操作数和操作码加一起只占一个字节。比如iconst_2对应的十六进制为0x05。-1比较特殊,对应的指令是iconst_m1(0x02)。
- 若n在[-128,127]范围内,使用bipush n的方式,操作数和操作码一起只占两个字节。比如n值为100(0x64)时,bipush 100对应十六进制为0x1064。
- 若n在[-32768,32767]范围内,使用sipush n的方式,操作数和操作码一起只占三个字节,比如n值为1024(0x0400)时,对应的字节码为sipush1024(0x110400)。
- 若n在其他范围内,则使用ldc的方式,这个范围的整数值被放在常量池中,比如n值为40000时,40000被存储到常量池中,加载的指令为ldc#i,i为常量池的索引值。
如下代码
public class TestByteCode {
private int int_2_2_1 = 1;
public void test2_1_1_1(int i,int j){
int k = int_2_2_1 + i +j;
System.out.println(k);
}
public void test2_1_1_2(){
int i = 1;
int j = 2;
int k = int_2_2_1 + i +j;
System.out.println(k);
}
public static void main(String[] args) {
}
}
复制代码


可以看到局部变量表有4个slot,1,2,3分别是我们制造的i,j,k
Code数据为
0 aload_0
1 getfield #2 <com/zifang/util/core/TestByteCode.int_2_2_1>
4 iload_1
5 iadd
6 iload_2
7 iadd
8 istore_3
9 getstatic #3 <java/lang/System.out>
12 iload_3
13 invokevirtual #4 <java/io/PrintStream.println>
16 return
复制代码
0 行将this压入操作数栈
1行调用getfield指令获得int_2_2_1变量的数据,压入操作数栈
4行,从局部变量表内1的位置(就是i)的值压入操作数栈
5行,调用iadd将 int_2_2_1 与 i的值进行累加,再压入操作数栈
6行,从iload_2将局部变量表内位置为2的值(j)压入栈
7行,累加,累加值压入栈
8行,调用istore_3指令,将最终的值存入位置为3的(k的位置)的局部变量表
9行,获得System.out的类ref压入栈
12行,从k上把数据压入栈
13行,执行invokevirtual进行调用
16行,执行空方法返回
为了说明const_*指令,制造了test2_1_1_2方法

各位可以自行分析
2.1.2 操作数栈指令
常见的操作数栈指令有pop、dup和swap。pop指令用于将栈顶的值出栈,一个常见的场景是调用了有返回值的方法,但是没有使用这个返回值,比如下面的代码。
public int test2_1_2_1(){
int i = 1;
int j = 2;
int k = int_2_2_1 + i +j;
return k;
}
public static void main(String[] args) {
new TestByteCode().test2_1_2_1();
}
复制代码
mian方法的字节码为:
0 new #5 <com/zifang/util/core/TestByteCode>
3 dup
4 invokespecial #6 <com/zifang/util/core/TestByteCode.<init>>
7 invokevirtual #7 <com/zifang/util/core/TestByteCode.test2_1_2_1>
10 pop
11 return
复制代码
其他指令大同小异,都是针对操作数栈本身的一些操作。这儿偷个懒,以后再全量的补充上。
2.2 运算和类型转换指令
java里面可以对数据进行加减乘除,也可以做与或非操作,如下操作与对应的字节码

我们java内如果有这样的语句: 1+1.0f,2是会转型变成float格式的,本质上是因为fadd指令只能接受两个float值,因此在字节码层面会使用i2f指令将int转换为float
fconst_1 // 将 1.0 入栈
iconst_1 // 将 1 入栈
i2f // 将栈顶的 1 的int 转为 float
fadd // 两个 float值相加
复制代码
而boolean、char、byte、short虽然是不同的数据类型,但是在JVM层面它们都被当作int来处理,不需要显式转为int,字节码指令上也没有对应转换的指令。

这里有些指令比较特殊,自增指令(和自减指令套路一样):iinc,接受两个参量,a,b,a指局部变量表的位置,b是累加值。因此你会发现这个指令是直接操作局部变量表,栈顶的信息是没有动的。因此会出现很多奇奇怪怪的问题。
- i++实例
public static void test_2_2_1() {
int i = 0;
for (int j = 0; j < 50; j++) {
i = i++;
}
System.out.println(i);
}
复制代码
查看字节码,获得到最核心的字节码
10 iload_0
11 iinc 0 by 1
14 istore_0
复制代码
可以看到iload_0 把i=0的数据放到了操作数栈,然后自增,直接操作了局部变量表,这个时候局部变量表上的i已经变成1了,但是后面的istore_0又把操作数栈上i=0赋值给局部变量表,导致i=0,因此无论遍历多少次,都是0。
- ++i实例
还是一样的套路,制造一个方法出来
public static void test_2_2_2() {
int i = 0;
for (int j = 0; j < 50; j++) {
i = ++i;
}
System.out.println(i);
}
复制代码
把最核心的字节码捞出来
10 iinc 0 by 1
13 iload_0
14 istore_0
复制代码
你会看到,这回的字节码顺序和之前的不一样了,先是针对局部变量表,将数据自增,完成后load到操作数栈,然后再保存回操作数栈,修改是生效的。
2.3 流程控制
控制转移指令根据条件进行分支跳转,我们常见的 if-then-else、三目表达式、for 循环、异常处理等都属于这个范畴。对应的指令集包括:
- 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、 if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq 和 if_acmpne。
- 复合条件分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
2.3.1 if判断分支
我们制造这样的代码:
public int test2_3_1(int n){
if(n > 0){
return 1;
} else {
return 0;
}
}
复制代码
得到的字节码为:
0 iload_1
1 ifle 6 (+5)
4 iconst_1
5 ireturn
6 iconst_0
7 ireturn
复制代码
这里起到关键性作用的就是ifile指令。它的作用是将操作数栈顶元素出栈跟0进行比较,如果小于等于0则跳转到特定的字节码处,如果大于0则继续执行接下来的字节码。
1行里面,和栈顶的数据(iload1,从局部变量表1的位置上拿到n值)与0进行比较,晓宇0则跳到6行
其他类似指令套路一样,大家可以自行尝试一把。其他指令参考下表

2.3.2 for循环
我们制造如下java代码:
public void test_2_3_2(int[] c){
for(int i = 0; i < c.length; i++){
System.out.println(i);
}
}
复制代码
反编译之后生成:
0 iconst_0
1 istore_2
2 iload_2
3 aload_1
4 arraylength
5 if_icmpge 21 (+16)
8 getstatic #3 <java/lang/System.out>
11 iload_2
12 invokevirtual #4 <java/io/PrintStream.println>
15 iinc 2 by 1
18 goto 2 (-16)
21 return
复制代码
局部变量表为

0行:iconst_0,0压入栈(相当于i = 0的赋值操作)
1行:把0的数据压到index2的局部变量表slot上(i的坑位)
2行:iload2指令,把局部变量表的i值压入操作数栈
3行:aload_1指令把c数组压栈
4行:针对数据获得长度,压栈(c已经不在栈上了)
5行:if_icmpge指令比较栈顶的两个值(数组长度与i数据),即代码里面的 i < c.length是否成立,成立则继续执行,不成立则直接跳转21
8-12相当于 System.out.print(i);
15行进行自增,然后跳转到2行,再次进行比较
这样的逻辑路线组成了我们的for循环语句
2.3.2.1 for-each循环原理
java里面针对for循环做个改进,使用了语法糖来表达循环语义,例如
public void test_2_3_2_1_1(){
int[] numbers = new int[]{1, 2, 3};
for (int number : numbers) {
System.out.println(number);
}
}
public void test_2_3_2_1_2(){
List<String> a = new ArrayList<>();
a.add("a");
a.add("b");
a.add("c");
for (String item : a) {
System.out.println(item);
}
}
复制代码
test_2_3_2_1_1方法编译后得到的字节码是
0 iconst_3
1 newarray 10 (int)
3 dup
4 iconst_0
5 iconst_1
6 iastore
7 dup
8 iconst_1
9 iconst_2
10 iastore
11 dup
12 iconst_2
13 iconst_3
14 iastore
15 astore_1
16 aload_1
17 astore_2
18 aload_2
19 arraylength
20 istore_3
21 iconst_0
22 istore 4
24 iload 4
26 iload_3
27 if_icmpge 50 (+23)
30 aload_2
31 iload 4
33 iaload
34 istore 5
36 getstatic #3 <java/lang/System.out>
39 iload 5
41 invokevirtual #4 <java/io/PrintStream.println>
44 iinc 4 by 1
47 goto 24 (-23)
50 return
复制代码
局部变量表为:

你会发现复杂了很多,但是看上去事实上和for循环没什么两样,各位可以使用已经知道的知识进行推演。
使用反编译工具获得到test_2_3_2_1_1方法的源码:
public void test_2_3_2_1_1() {
int[] numbers = new int[]{1, 2, 3};
int[] var2 = numbers;
int var3 = numbers.length;
for(int var4 = 0; var4 < var3; ++var4) {
int number = var2[var4];
System.out.println(number);
}
}
复制代码
你会发现,本质上就是for循环,编译器在执行生成字节码的时候,会针对语法糖进行解糖操作。
用同样的方式一步一步分析test_2_3_2_1_2方法获得到反编译出来的源码:
public void test_2_3_2_1_2() {
List<String> a = new ArrayList();
a.add("a");
a.add("b");
a.add("c");
Iterator var2 = a.iterator();
while(var2.hasNext()) {
String item = (String)var2.next();
System.out.println(item);
}
}
复制代码
你会发现它使用了迭代器模式进行的功能循环。
迭代器接口位于Collection接口之上,意味着只要是我们java里面的集合类,都可以使用for-each循环带来的便捷。
2.3.3 swith-case分支
2.3.3.1 switch 跳转
同样的套路制造一个java代码
public int test_2_3_3_1_1(int i) {
switch (i) {
case 100: return 0;
case 101: return 1;
case 104: return 4;
default: return -1;
}
}
复制代码
用javap反编译字节码:
0 iload_1
1 tableswitch 100 to 104 100: 36 (+35)
101: 38 (+37)
102: 42 (+41)
103: 42 (+41)
104: 40 (+39)
default: 42 (+41)
36 iconst_0
37 ireturn
38 iconst_1
39 ireturn
40 iconst_4
41 ireturn
42 iconst_m1
43 ireturn
复制代码
可以看到,多了102与103,这部分是虚拟机自己帮忙填补上去的,指向default语句分支。这样可以实现 O(1) 时间复杂度的查找,通过游标就可以一次找到。
那么如果case里面的数值相差比较大,断层了怎么办呢?
public int test_2_3_3_1_2(int i) {
switch (i) {
case 1: return 0;
case 10: return 1;
case 100: return 4;
default: return -1;
}
}
复制代码
以下对应的字节码
0 iload_1
1 lookupswitch 3
1: 36 (+35)
10: 38 (+37)
100: 40 (+39)
default: 42 (+41)
36 iconst_0
37 ireturn
38 iconst_1
39 ireturn
40 iconst_4
41 ireturn
42 iconst_m1
43 ireturn
复制代码
你会发现这个时候使用的是lookupswitch指令。它的键值都是经过排序的,在查找上可以采用二分查找的方式,时间复杂度为 O(log n)
所谓稀疏是指javac下会对tableswitch 和 lookupswitch 进行代价的估算,算法的原因可能会导致少数选项下不会特别的区分开来。
2.3.3.2 String-switch
public int test_2_3_3_2(String name) {
switch (name) {
case "吃饭1":
return 100;
case "吃饭2":
return 200;
default:
return -1;
}
}
复制代码
0 aload_1
1 astore_2
2 iconst_m1
3 istore_3
4 aload_2
5 invokevirtual #16 <java/lang/String.hashCode>
8 lookupswitch 2
21885863: 36 (+28)
21885864: 50 (+42)
default: 61 (+53)
36 aload_2
37 ldc #17 <吃饭1>
39 invokevirtual #18 <java/lang/String.equals>
42 ifeq 61 (+19)
45 iconst_0
46 istore_3
47 goto 61 (+14)
50 aload_2
51 ldc #19 <吃饭2>
53 invokevirtual #18 <java/lang/String.equals>
56 ifeq 61 (+5)
59 iconst_1
60 istore_3
61 iload_3
62 lookupswitch 2
0: 88 (+26)
1: 91 (+29)
default: 95 (+33)
88 bipush 100
90 ireturn
91 sipush 200
94 ireturn
95 iconst_m1
96 ireturn
复制代码
反编译之后是这样的:
public int test_2_3_3_2(String name) {
byte var3 = -1;
switch(name.hashCode()) {
case 21885863:
if (name.equals("吃饭1")) {
var3 = 0;
}
break;
case 21885864:
if (name.equals("吃饭2")) {
var3 = 1;
}
}
switch(var3) {
case 0:
return 100;
case 1:
return 200;
default:
return -1;
}
}
复制代码
可以看到,编译的时候会将case里面的数据拿出来hash化,为了防止hash冲突,在case下使用if真正确定逻辑的分支
2.4 面向对象编程
2.4.1 对象初始化指令
简单的说,涉及到对象初始化的指令有三个
new<init><clinit>
而对象初始化又可以分为对象初始化与类的静态初始化
2.4.1.1 对象初始化
例如以下代码
A a = new A();
复制代码
反编译字节码之后会有
0: new #2 // class A
3: dup
4: invokespecial #3 // Method A."<init>":()V
7: astore_1
复制代码
一个对象创建的语句肯定是new,dup,invokespecial的三连语句。
new 只会创建一个object的实例,但是必须经过invokespecial指令间接调用<init>之后才能承认这个Object是ok的。而new 完先塞到栈顶,在执行invokespecial之后把栈顶的初始化好了的object出栈。因此会导致astore_1没有东西存了,所以这个时候需要把栈顶的数据dup一下。
dup 是duplicate的缩写,意思就是复制,双倍。将栈顶的东西复制一遍,再压进去。
invokespecial就可以从栈顶拿到刚刚的Object了。
2.4.1.2 类的静态初始化
<clinit>是类的静态初始化,比<init>早一点,不会直接被调用。
它在下面这个四个指令触发调用:new, getstatic, putstatic , invokestatic。
也就是说,初始化一个类实例、访问一个静态变量或者一个静态方法,类的静态初始化方法就会被触发。
例如:
public class Initializer {
static int a;
static int b;
static {
a = 1;
b = 2;
}
}
// 部分字节码如下
static {};
0: iconst_1
1: putstatic #2 // Field a:I
4: iconst_2
5: putstatic #3 // Field b:I
8: return
复制代码
2.4.1.3 常见面试题从字节码角度解决
- A a = new B(); 输出结果及正确的顺序
public class A {
static {
System.out.println("A init");
}
public A() {
System.out.println("A Instance");
}
}
public class B extends A {
static {
System.out.println("B init");
}
public B() {
System.out.println("B Instance");
}
}
复制代码
public B();
0: aload_0
1: invokespecial #1 // Method A."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String B Instance
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
复制代码
被动驱动的逻辑先行,B初始化驱动B的类静态初始化,B的静态初始化会驱动父类的类的静态初始化。
子类初始化必须先调用父类的初始化方法,因此就会有:
a)A的静态初始化
b)B的类的静态初始化
c) A类的实例初始化
d)B类的实例初始化

- B[] arr = new B[10]的输出
bipush 10
anewarray 'B'
astore 1
复制代码
可以看到没有使用任何和初始化相关的的指令自然没有进行初始化的必要。
2.4.2 方法调用指令
之前已经接触过了invokespecial,和他一样麻烦的的还有4个,这tm的。
- invokestatic:用于调用静态方法
- invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法
- invokevirtual:用于调用非私有实例方法
- invokeinterface:用于调用接口方法
- invokedynamic:用于调用动态方法
技术存在是有目的性的,肯定是为了解决实实在在的需求。为什么一个简单的方法调用会分裂成5兄弟呢?
你看java里面只要在编译器阶段就能确定了行为的,那就可以说是静态绑定。需要在运行时根据调用者的类型动态识别的叫动态绑定
——这个术语太难了,以前我是不能理解的,你运行期怎么干的,我看代码也能猜出来的,JVM没理由不知道啊。
咱们换个说法——你看,构造器不能被覆盖吧,对这就是静态绑定的,static方法不能被覆盖吧,对,这也是静态绑定的——其他的,统统都是动态的!什么多态啊,子类重写父类啊,什么的,统统都是静态绑定的!
那么好了,invokestatic 和 invokespecial 这俩就是负责,调用静态绑定方法,和构造器(包括private方法,不能被覆盖的静态绑定方法)
剩下的都是调用动态绑定的方法的。
2.4.2.1 invokevirtual
用来调用 public、protected、package 访问级别的方法。如下:
public class Color {
public void printColorName() {
System.out.println("Color name from parent");
}
}
public class Red extends Color {
@Override
public void printColorName() {
System.out.println("Color name is Red");
}
}
public class Yellow extends Color {
@Override
public void printColorName() {
System.out.println("Color name is Yellow");
}
}
public class InvokeVirtualTest {
private static Color yellowColor = new Yellow();
private static Color redColor = new Red();
public static void main(String[] args) {
yellowColor.printColorName();
redColor.printColorName();
}
}
输出
Color name is Yellow
Color name is Red
复制代码
以下是字节码
0: getstatic #2 // Field yellowColor:LColor;
3: invokevirtual #3 // Method Color.printColorName:()V
6: getstatic #4 // Field redColor:LColor;
9: invokevirtual #3 // Method Color.printColorName:()V
复制代码
可以看到 3 和 9 行指令完全一样,都是Color.printColorName,并没有被编译器改写为Yellow.printColorName和Red.printColorName。它们最终调用的目标方法却不同,invokevirtual 会根据对象的实际类型进行分派(虚方法分派),在编译期间不能确定最终会调用子类还是父类的方法。
2.4.2.2 invokeinterface
看名字就知道专门调用接口方法的,同样是调用动态绑定方法的。
那它与invokevirtual有什么区别,为什么不用invokevirtual来实现接口方法的调用呢?
这里需要引入一个新的概念:Java方法分派
在讨论多态(一般叫运行时多态)的时候,不可避免地要和重载(Overload)进行对比
-
虚拟机会在类的方法区建立一个虚拟方法表的数据结构(virtual method table,vtable)。
方法表会在类的连接阶段初始化,方法表存储的是该类方法入口的一个映射,如果子类继承了父类,但是某个父类的方法没有被子类重写,那么在子类的方法表里边该方法指向的是父类的方法的入口,子类并不会重新生成一个方法,然后让方法表去指向这个生成的,这样做是没有意义的。还有一点,如果子类重写了父类的方法,那么子类这个被重写的方法的索引和父类的该方法的索引是一致。这样做的目的是为了快速查找,比如说在子类里边找不到一个方法索引为1的方法,那么jvm会直接去父类查找方法索引为1的方法,不需要重新在父类里边遍历。
-
针对于invokeinterface指令来说,虚拟机会建立一个叫做接口方法表的数据结构(interface method table,itable)
在需要调用某个接口方法时,虚拟机会在itable的offset table中查找到对应的方法表位置和方法位置,随后在method table中查找具体的方法实现。invokevirtual的实现依赖于Java的单继承特性,子类的虚方法表保留了父类虚方法表的顺序,但是因为Java的多接口实现,这一特性无法使用。
vtable、itable机制是实现Java多态的基础。
- 子类会继承父类的vtable。因为Java类都会继承Object类,Object中有5个方法可以被继承,所以一个空Java类的vtable的大小也等于5。
- 被final和static修饰的方法不会出现在vtable中,因为没有办法被继承重写,同理可以知道private修饰的方法也不会出现在vtable中。
- 接口方法的调用使用invokeinterface指令,Java使用itable来支持多接口实现,itable由offset table和method table两部分组成。在调用接口方法时,会先在offset table中查找method table的偏移量位置,随后在method table查找具体的接口实现。
2.4.2.3 invokeDynamic
jdk7引入,那个时候自己不用,然后groovy,JRuby,Kotlin之类的就开始开花了,璀璨的不得了。JDK8的时候lamda表达式终于开始用上这个指令了。他相当于编译时谁都不知道执行什么,真正的逻辑被下放到用户的代码里头。
这里会有一个非常重要的核心类:MethodHandle。
MethodHandle 又被称为方法句柄或方法指针, 是java.lang.invoke 包中的 一个类,它的出现使得 Java 可以像其它语言一样把函数当做参数进行传递。这个东西看其里很像Method反射,但是比起Method更为轻量级,也能享受到jit带来的提效。
下面以一个实际的例子来看 MethodHandle 的用法
public class Foo {
public void print(String s) {
System.out.println("hello, " + s);
}
public static void main(String[] args) throws Throwable {
Foo foo = new Foo();
MethodType methodType = MethodType.methodType(void.class, String.class);
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(Foo.class, "print", methodType);
methodHandle.invokeExact(foo, "world");
}
}
// 运行输出
hello, world
复制代码
使用 MethodHandle 的方法的步骤是:
- 创建 MethodType 对象。MethodType 用来表示方法签名,每个 MethodHandle 都有一个 MethodType 实例,用来指定方法的返回值类型和各个参数类型
- 调用 MethodHandles.lookup 静态方法返回
MethodHandles.Lookup对象,这个对象是查找的上下文,根据方法的不同类型通过 findStatic、findSpecial、findVirtual 等方法查找方法签名为 MethodType 的方法句柄 - 拿到方法句柄以后就可以执行了。通过传入目标方法的参数,使用
invoke或者invokeExact就可以进行方法的调用。
例如kotlin或者是groovy,都有点类似于脚本语言,而脚本语言和静态语言的区别就是,入参随便放,能不能执行要去执行之后才知道
这里我们制造一个Test.groovy出来
def add(a, b) {
new Exception().printStackTrace()
return a + b
}
复制代码
这里groovy在将代码翻译成.class就遇到了问题。a,b我不知道是个啥啊?我怎么造方法呢?那就用invoke_dynamic,使用invoke_dynamic指令之后,就可以将代码翻译成这样:
public static void main(String[] args) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(Object.class,Object.class, Object.class);
CallSite callSite = IndyInterface.bootstrap(lookup, "invoke", mt,"add", 0);
MethodHandle mh = callSite.getTarget();
mh.invokeExact(obj, "hello", "world");
}
复制代码
这里的IndyInterface是groovy提供的切入口,那么释放给groovy的权利就很大了,直接可以控制具体怎么执行,下发到groovy提供的jar里面。
当然,invoke_dynamic指令不单是在不同类语言中大放异彩,在java8引入的lamda表达式内同样可以看到其风采。
2.5 异常处理机制
我们制造一个异常处理的代码:
public void test2_5_1_exception(){
throw new RuntimeException();
}
public void test2_5_1_handler(Exception e){
System.out.println("捕获到异常");
}
public void test2_5_1(){
try {
test2_5_1_exception();
} catch (RuntimeException e) {
test2_5_1_handler(e);
}
}
复制代码
得到的字节码为:
0 aload_0
1 invokevirtual #23 <com/zifang/util/core/TestByteCode.test2_5_1_exception>
4 goto 13 (+9)
7 astore_1
8 aload_0
9 aload_1
10 invokevirtual #24 <com/zifang/util/core/TestByteCode.test2_5_1_handler>
13 return
复制代码
这里还有异常表信息,为:

在编译后字节码中,每个方法都附带一个异常表(Exception table),异常表里的每一行表示一个异常处理器,由 from 指针、to 指针、target 指针、所捕获的异常类型 type 组成。这些指针的值是字节码索引,用于定位字节码 其含义是在[from, to)字节码范围内,抛出了异常类型为type的异常,就会跳转到target表示的字节码处。 比如,上面的例子异常表表示:在0到4中间(不包含 4)如果抛出了RuntimeException的异常,就跳转到7执行。
有很多个catch的时候
public void test2_5_2(){
try {
test2_5_1_exception();
} catch (NullPointerException e) {
test2_5_1_handler(e);
}catch (RuntimeException e){
test2_5_1_handler(e);
}
}
复制代码

可以看到多一个异常,会在异常表(Exception table 里面多一条记录)。
当程序出现异常时,Java 虚拟机会从上至下遍历异常表中所有的条目。当触发异常的字节码索引值在某个异常条目的[from, to)范围内,则会判断抛出的异常与该条目想捕获的异常是否匹配。
- 如果匹配,Java 虚拟机会将控制流跳转到 target 指向的字节码;如果不匹配则继续遍历异常表
- 如果遍历完所有的异常表,还未匹配到异常处理器,那么该异常将蔓延到调用方(caller)中重复上述的操作。最坏的情况下虚拟机需要遍历该线程 Java 栈上所有方法的异常表
如果增加finally
public void test2_5_3(){
try {
test2_5_1_exception();
} catch (NullPointerException e) {
test2_5_1_handler(e);
}finally {
test2_5_finally();
}
}
public void test2_5_finally(){
System.out.println("finally语句块");
}
复制代码
获得到字节码与异常表为:
0 aload_0
1 invokevirtual #24 <com/zifang/util/core/TestByteCode.test2_5_1_exception>
4 aload_0
5 invokevirtual #27 <com/zifang/util/core/TestByteCode.test2_5_finally>
8 goto 31 (+23)
11 astore_1
12 aload_0
13 aload_1
14 invokevirtual #25 <com/zifang/util/core/TestByteCode.test2_5_1_handler>
17 aload_0
18 invokevirtual #27 <com/zifang/util/core/TestByteCode.test2_5_finally>
21 goto 31 (+10)
24 astore_2
25 aload_0
26 invokevirtual #27 <com/zifang/util/core/TestByteCode.test2_5_finally>
29 aload_2
30 athrow
31 return
复制代码

可以看到,字节码中包含了三份 finally 语句块,都在程序正常 return 和异常 throw 之前。其中两处在 try 和 catch 调用 return 之前,一处是在异常 throw 之前。
Java 采用方式是复制 finally 代码块的内容,分别放在 try catch 代码块所有正常 return 和 异常 throw 之前。
因此人肉翻译一下上面的字节码的等效代码:
public void foo() {
try {
test2_5_1_exception();
test2_5_finally();
} catch (NullPointerException e) {
try {
test2_5_1_handler(e);
} catch (Throwable e2) {
test2_5_finally();
throw e2;
}
} catch (Throwable e) {
test2_5_finally();
throw e;
}
}
复制代码
好有了这个基础之后,试分析:
public int test2_5_3_1() {
try {
int a = 1 / 0;
return 0;
} catch (Exception e) {
int b = 1 / 0;
return 1;
} finally {
return 2;
}
}
public int test2_5_3_2() {
int i = 100;
try {
return i;
} finally {
++i;
}
}
public int test2_5_3_3() {
int i = 100;
try {
return i;
} finally {
i++;
}
}
public String test2_5_3_4() {
String s = "hello";
try {
return s;
} finally {
s = "xyz";
}
}
复制代码
这几个方法都会返回什么?
2.6 泛型原理
制造如下的代码
public class Pair<T> {
public T first;
public T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
}
public void foo(Pair<String> pair) {
String left = pair.left;
}
复制代码
获得到foo的字节码为:
0: aload_1
1: getfield #2 // Field left:Ljava/lang/Object;
4: checkcast #4 // class java/lang/String
7: astore_2
8: return
复制代码
可以看到left字段的字段类型为Object,并不是String。
checkcast指令用来检查对象是否符合给定类型,如果不符合条件,则抛出java.lang.ClassCastException异常。
将上面的代码翻译过来就是:
public class Pair<T> {
public Object first;
public Object second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
}
public void foo(Pair pair) {
String left = (String)pair.left;
}
复制代码
理解泛型概念的最重要的是理解类型擦除。
例如在一个类内定义:
public void print(List<String> list) { }
public void print(List<Integer> list) { }
复制代码
这样是会在编译期报错的,真正的代码内都已经变成了print(List list),而JVM不允许相同签名的方法在一个类中同时存在,所以上面代码编译会失败。
2.7 lambda表达式原理
2.7.1 匿名内部类实现方式
public void test2_7_1() {
Runnable r1 = new Runnable() {
public void run() {
System.out.println("hello, inner class");
}
};
r1.run();
}
复制代码
生成的字节码是:
0 new #31 <com/zifang/util/core/TestByteCode$1>
3 dup
4 aload_0
5 invokespecial #32 <com/zifang/util/core/TestByteCode$1.<init>>
8 astore_1
9 aload_1
10 invokeinterface #33 <java/lang/Runnable.run> count 1
15 return
复制代码
人肉一把看出执行的代码为
class TestByteCode$1 implements Runnable {
public TestByteCode$1(TestByteCode var) {
}
@Override
public void run() {
System.out.println("hello, inner class");
}
public class TestByteCode {
public void test2_7_1(){
Runnable r1 = new TestByteCode$1(this);
r1.run();
}
}
复制代码
相当于匿名内部类会生成出一个class类,将自身传递到这个类内,然后由外部类调用。

2.7.2 lambda 表达式实现的方式
public void test2_7_2(){
Runnable r1 = () -> System.out.println("hello, inner class");
r1.run();
}
复制代码
使用java -p -v (不然lambdatest2_7_20不会出现) 反编译得到字节码为
public void test2_7_2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #34, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #33, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return
LineNumberTable:
line 197: 0
line 198: 6
line 199: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 this Lcom/zifang/util/core/TestByteCode;
6 7 1 r1 Ljava/lang/Runnable;
private static void lambda$test2_7_2$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #44 // String hello, inner class
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 197: 0
BootstrapMethods:
0: #161 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#162 ()V
#163 invokestatic com/zifang/util/core/TestByteCode.lambda$test2_7_2$0:()V
#162 ()V
复制代码
可以看到多了一个lambdatest2_7_20的static方法,人肉一把:
private static void lambda$main$0() {
System.out.println("hello, inner class");
}
复制代码
当前常量池为:
Constant pool:
#34 = InvokeDynamic #0:#164 // #0:run:()Ljava/lang/Runnable;
#164 = NameAndType #197:#200 // run:()Ljava/lang/Runnable;
BootstrapMethods:
0: #161 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#162 ()V
#163 invokestatic com/zifang/util/core/TestByteCode.lambda$test2_7_2$0:()V
#162 ()V
复制代码
其中#0是一个特殊的查找,对应 BootstrapMethods 中的 0 行,可以看到这是一个对静态方法 LambdaMetafactory.metafactory() 的调用,它的返回值是 java.lang.invoke.CallSite 对象,这个对象代表了真正执行的目标方法调用。
// LambdaMetafactory#metafactory 方法
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
复制代码
- caller:表示JVM提供的查找上下文。
- invokedName:表示调用函数名,在本例中invokedName为“run”。
- samMethodType:表示函数式接口定义的方法签名(参数类型和返回值类型),本例中run方法的签名为“()void”。
- implMethod:表示编译时生成的Lambda表达式对应的静态方法invokestatic TestByteCode.lambdatest2_7_20
- instantiatedMethodType:一般和samMethodType是一样或是它的一个特例,在本例中是“()void”。
这里最重要也是最复杂的是:InnerClassLambdaMetafactory方法调用
public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
MethodType invokedType,
String samMethodName,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType,
boolean isSerializable,
Class<?>[] markerInterfaces,
MethodType[] additionalBridges)
throws LambdaConversionException {
super(caller, invokedType, samMethodName, samMethodType,
implMethod, instantiatedMethodType,
isSerializable, markerInterfaces, additionalBridges);
implMethodClassName = implDefiningClass.getName().replace('.', '/');
implMethodName = implInfo.getName();
implMethodDesc = implMethodType.toMethodDescriptorString();
implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
? implDefiningClass
: implMethodType.returnType();
constructorType = invokedType.changeReturnType(Void.TYPE);
lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
int parameterCount = invokedType.parameterCount();
if (parameterCount > 0) {
argNames = new String[parameterCount];
argDescs = new String[parameterCount];
for (int i = 0; i < parameterCount; i++) {
argNames[i] = "arg$" + (i + 1);
argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
}
} else {
argNames = argDescs = EMPTY_STRING_ARRAY;
}
}
复制代码
在这个方法内部默默地生成了内部类,类名的规则是ClassName$$Lambda$n。其中ClassName是Lambda所在的类名,后面的数字n按生成的顺序依次递增。这个类在底层是使用asm的方式生成的,类的样子是:
final class TestByteCode$$Lambda$1 implements Runnable {
@Override
public void run() {
TestByteCode.lambda$test2_7_2$0();
}
}
复制代码
因此整体而言,dynamic的执行顺序是:
- lambda 表达式声明的地方会生成一个 invokedynamic 指令,同时编译器生成一个对应的引导方法(Bootstrap Method)
- 第一次执行 invokedynamic 指令时,会调用对应的引导方法(Bootstrap Method),该引导方法会调用 LambdaMetafactory.metafactory 方法动态生成内部类
- 引导方法会返回一个动态调用 CallSite 对象,这个 CallSite 会链接最终调用的实现了 Runnable 接口的内部类
- lambda 表达式中的内容会被编译成静态方法,前面动态生成的内部类会直接调用该静态方法
- 真正执行 lambda 调用的还是用 invokeinterface 指令
public class TestByteCode{
public void test2_7_2(){
Runnable r1 = () -> System.out.println("hello, inner class");
r1.run();
}
}
复制代码
public class TestByteCode{
public void test2_7_2(){
// 这里的代码等价于 一大堆 CallSite的类似于method的方法句柄调用
call TestByteCode$$Lambda$1 # run()
}
}
private static void lambda$main$0() {
System.out.println("hello, inner class");
}
final class TestByteCode$$Lambda$1 implements Runnable {
@Override
public void run() {
TestByteCode.lambda$test2_7_2$0();
}
}
复制代码
2.8 synchronized实现原理
Synchronized 太重要了,是并发编程里面根本绕不开的坎。
当我们使用Synchronized关键字包裹一个Object的时候:
private Object lock = new Object();
public void foo() {
synchronized (lock) {
bar();
}
}
public void bar() { }
复制代码
反编译后:
public void foo();
Code:
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: aload_0
8: invokevirtual #4 // Method bar:()V
11: aload_1
12: monitorexit
13: goto 21
16: astore_2
17: aload_1
18: monitorexit
19: aload_2
20: athrow
21: return
Exception table:
from to target type
7 13 16 any
16 19 16 any
复制代码
- 0 ~ 5:将 lock 对象入栈,使用 dup 指令复制栈顶元素,并将它存入局部变量表位置 1 的地方,现在栈上还剩下一个 lock 对象
- 6:以栈顶元素 lock 做为锁,使用 monitorenter 开始同步
- 7 ~ 8:调用 bar() 方法
- 11 ~ 12:将 lock 对象入栈,调用 monitorexit 释放锁
当我们使用synchronized修饰一个方法时:
synchronized public void testMe() {
}
// 对应字节码
public synchronized void testMe();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
复制代码
JVM 不会使用特殊的字节码来调用同步方法,当 JVM 解析方法的符号引用时,它会判断方法是不是同步的(检查方法 ACC_SYNCHRONIZED 是否被设置)。如果是,执行线程会先尝试获取锁。如果是实例方法,JVM 会尝试获取实例对象的锁,如果是类方法,JVM 会尝试获取类锁。在同步方法完成以后,不管是正常返回还是异常返回,都会释放锁。
2.10 字节码与反射
针对同一个方法反射执行超过一定的上限之后,将会使用asm制造出一个新的类来调用。详情以后吧,我累了。
3. 小结
没有小结,我累了























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)