本篇为《JVM 指令分析实例》的第四篇, 相关实例均使用 Oracle JDK 1.8 编译, 并使用 javap 生成字节码指令清单.
前几篇传送门:
JVM 指令分析实例一(常量, 局部变量, for 循环) https://mp.weixin.qq.com/s/anzDcV01y13pdFP7U42G-w
JVM 指令分析实例二(算术运算, 常量池, 控制结构) https://mp.weixin.qq.com/s/vuDiJlJloYtjLi-Sm9u65A
JVM 指令分析实例三(方法调用, 类实例) https://mp.weixin.qq.com/s/gmgaYNM8TsluhXElyeuCYg
数组
一维原始类型数组
- void createBuffer() {
- int buffer[];
- int bufsz = 100;
- int value = 12;
- buffer = new int[bufsz];
- buffer[10] = value;
- value = buffer[11];
- }
字节码指令序列
- void createBuffer():
- 0: bipush 100 // 将单字节 int 常量值 100 压入栈顶
- 2: istore_2 // 将栈顶 int 类型数值 100 存入第 3 个局部变量. bufsz = 100
- 3: bipush 12 // 将单字节 int 常量值 12 压入栈顶
- 5: istore_3 // 将栈顶 int 类型数值 12 存入第 4 个局部变量. value = 12
- 6: iload_2 // 将第 3 个 int 类型局部变量压入栈顶
- 7: newarray int // 创建 int 类型数组, 并将数组引用值压入栈顶. new int[bufsz]
- 9: astore_1 // 将栈顶引用类型值存入第 2 个局部变量. buffer = new int[bufsz]
- 10: aload_1 // 将第 2 个引用类型局部变量压入栈顶
- 11: bipush 10 // 将单字节 int 常量 10 压入栈顶
- 13: iload_3 // 将第 4 个 int 类型局部变量压入栈顶
- 14: iastore // 将栈顶 int 类型数值存入数组的指定索引位置. buffer[10] = value
- 15: aload_1 // 将第 2 个引用类型值压入栈顶
- 16: bipush 11 // 将单字节 int 常量值 11 压入栈顶
- 18: iaload // 将 int 类型数组的指定元素压入栈顶
- 19: istore_3 // 将栈顶 int 类型数值存入第 4 个局部变量
- 20: return
newarray 指令
创建一个指定原始类型 (如 int,float,char 等) 的数组, 并将其引用值压入栈顶.
执行该指令后, 将从操作数栈出栈 1 个参数 count, 类型为 int, 表示要创建数组的大小.
iastore 指令
从操作数栈读取一个 int 类型数据并存入指定数组中.
执行该指令后, 将从操作数栈出栈 3 个参数 arrayref,index 和 value, 在本例中分别对应于第 10,11 和 13 索引位置压入的值.
其中, arrayref 是一个引用类型值, 指向一个 int 类型的数组. index 和 value 为 int 类型, index 表示待存入数组位置的索引号, value 表示待存入 index 索引位置的值.
iaload 指令
从数组中加载一个 int 类型数据到操作数栈.
执行该指令后, 将从操作数栈出栈 2 个参数 arrayref 和 index, 在本例中分别对应于第 15 和 16 索引位置压入的值.
其中, arrayref 是一个引用类型值, 指向一个 int 类型的数组. index 为 int 类型, 表示待加载数组数据的索引号.
一维引用类型数组
- void createThreadArray() {
- Thread threads[];
- int count = 10;
- threads = new Thread[count];
- threads[0] = new Thread();
- }
字节码指令序列
- void createThreadArray():
- 0: bipush 10 // 将单字节 int 类型值 10 压入栈顶
- 2: istore_2 // 将栈顶 int 类型值存入第 3 个局部变量. count = 10
- 3: iload_2 // 将第 3 个 int 类型局部变量压入栈顶
- 4: anewarray #15 // class java/lang/Thread. 创建 Thread 类型数组, 并将数组引用值压入栈顶. new Thread[count]
- 7: astore_1 // 将栈顶引用类型值存入第 2 个局部变量
- 8: aload_1 // 将第 2 个引用类型局部变量压入栈顶
- 9: iconst_0 // 将 int 类型常量 0 压入栈顶
- 10: new #15 // class java/lang/Thread. 创建 Thread 对象, 并将引用值压入栈顶
- 13: dup // 复制栈顶值并压入栈顶
- 14: invokespecial #17 // Method java/lang/Thread."<init>":()V. 调用实例初始化方法
- 17: aastore // 将栈顶引用类型值存入数组的指定索引位置. threads[0] = new Thread()
- 18: return
anewarray 指令
创建一个引用类型 (如类, 接口, 数组) 数组, 并将其引用值压入栈顶. 可用于创建一维引用数组, 或者用于创建多维数组的一部分.
执行该指令后, 将从操作数栈出栈 1 个参数 count, 类型为 int, 表示要创建数组的大小.
aastore 指令
(aastore 指令与 iastore 指令作用类似)
从操作数栈读取一个引用类型数据并存入指定数组中.
执行该指令后, 将从操作数栈出栈 3 个参数 arrayref,index 和 value, 在本例中分别对应于第 8,9 和 10 索引位置压入的值.
其中, arrayref 是一个引用类型值, 指向一个引用类型的数组. index 为 int 类型, index 表示待存入数组位置的索引号. value 为引用类型, 表示待存入 index 索引位置的值.
在运行时, value 的实际类型必须与 arrayref 所代表的数组的组件类型相匹配.
多维数组
- int[][][] create3DArray() {
- int grid[][][];
- grid = new int[10][5][];
- return grid;
- }
字节码指令序列
- int[][][] create3DArray():
- 0: bipush 10 // 将单字节 int 类型值 10 压入栈顶. 第 1 维
- 2: iconst_5 // 将 int 类型常量 5 压入栈顶. 第 2 维
- 3: multianewarray #16, 2 // class "[[[I". 创建 int[][][]类型数组, 并将引用值压入栈顶
- 7: astore_1 // 将栈顶引用类型值存入第 2 个局部变量
- 8: aload_1 // 将第 2 个引用类型局部变量压入栈顶
- 9: areturn // 从当前方法返回栈顶引用类型值
multianewarray 指令
创建指定类型和指定维度的多维数组(执行该指令时, 操作数栈中必须包含各维度的长度值), 并将其引用值压入栈顶. 可以用于创建所有类型的多维数组.
对于本实例, 数组类型为 [[[I, 即 #16 对应的常量池中的符号引用. 数组维度为 2, 两个维度的长度值分别为 10 和 5. 虽然 int[][][] 为 3 维数组, 但由于仅指定了前 2 个维度的长度值, 因此指令对应的维度值为 2.
如果指定了第 3 个维度的长度值, 那么在 iconst_5 之后还需要再将 1 个 int 类型长度值压入栈.
所有的数组都有一个与之关联的长度属性, 可通过 arraylength 指令访问.
switch 语句
编译器会使用 tableswitch 和 lookupswitch 指令来生成 switch 语句的编译代码.
Java 虚拟机的 tableswitch 和 lookupswitch 指令都只能支持 int 类型的条件值.
tableswitch 指令可以高效地从索引表中确定 case 语句块的分支偏移量.
当 switch 语句中的 case 分支条件值比较稀疏时, tableswitch 指令的空间使用率偏低. 这种情况下, 可以使用 lookupswitch 指令来代替.
tableswitch 指令
- int chooseNear(int i) {
- switch(i) {
- case 0: return 0;
- case 1: return 1;
- case 2: return 2;
- default: return -1;
- }
- }
字节码指令序列
- int chooseNear(int):
- 0: iload_1 // 将第 2 个 int 类型局部变量压入栈顶
- 1: tableswitch { // 0 to 2
- 0: 28 // 如果 case 条件值为 0, 则跳转到索引号为 28 的指令继续执行
- 1: 30 // 如果 case 条件值为 1, 则跳转到索引号为 30 的指令继续执行
- 2: 32 // 如果 case 条件值为 2, 则跳转到索引号为 32 的指令继续执行
- default: 34 // 否则, 则跳转到索引号为 34 的指令继续执行
- }
- 28: iconst_0 // 将 int 类型常量 0 压入栈顶
- 29: ireturn // 从当前方法返回栈顶 int 类型数值
- 30: iconst_1 // 将 int 类型常量 1 压入栈顶
- 31: ireturn // 从当前方法返回栈顶 int 类型数值
- 32: iconst_2 // 将 int 类型常量 2 压入栈顶
- 33: ireturn // 从当前方法返回栈顶 int 类型数值
- 34: iconst_m1 // 将 int 类型常量 - 1 压入栈顶
- 35: ireturn // 从当前方法返回栈顶 int 类型数值
tableswitch 指令
用于 switch 条件跳转, case 值连续(变长指令).
根据索引值在跳转表中寻找配对的分支并进行跳转.
指令格式: tableswitch padbytes defaultbytes lowbytes highbytes jumptablebytes
padbytes:0~3 个填充字节, 以使得 defaultbytes 与方法起始地址 (方法内第一条指令的操作码所在的地址) 之间的距离是 4 的位数.
defaultbytes:32 位默认跳转地址
lowbytes:32 位低值 low
highbytes:32 位高值 high
jumptablebytes:(high-low+1)个 32 位有符号数值形成的一张零基址跳转表(0-based jump table)
由于采用了索引值定位的方式(可理解为数组随机访问), 因此只需要检查索引是否越界, 非常高效.
下面结合实例分析一下:
第 1 条指令的索引号为 0,tableswitch 指令索引号为 1, 为了使 defaultbytes 与方法起始地址之间的距离是 4 的位数, 所以 defaultbytes 的开始索引号为 4.
defaultbytes,lowbytes 和 highbytes 分别占 4 个字节, 总共 12 个字节.
case 高低值分别为 2 和 0, 因此 jumptablebytes 占用(2-0+1)*4=12 个字节.
由于 defaultbytes 的开始索引号为 4,defaultbytes~jumptablebytes 共占用 24 个字节, 因此紧跟在 tableswitch 后面的下一条指令的索引号为 4+24=28, 对应于实例中的指令 "28: iconst_0".
这里顺便提一下, 一般情况下, 普通的操作数占 1 个字节, 指向常量池的索引值占 2 个字节(ldc 的常量池索引占 1 个字节, ldc_w,ldc2_w 的常量池索引占 2 个字节). 所以, 方法的指令索引号之间有时不是连续的.
lookupswitch 指令
- int chooseFar(int i) {
- switch(i) {
- case -100: return -1;
- case 0: return 0;
- case 100: return 1;
- default: return -1;
- }
- }
字节码指令序列
- int chooseFar(int):
- 0: iload_1
- 1: lookupswitch { // 3
- -100: 36
- 0: 38
- 100: 40
- default: 42
- }
- 36: iconst_m1
- 37: ireturn
- 38: iconst_0
- 39: ireturn
- 40: iconst_1
- 41: ireturn
- 42: iconst_m1
- 43: ireturn
lookupswitch 指令
用于 switch 条件跳转, case 值不连续(变长指令).
根据键值 (非索引) 在跳转表中寻找配对的分支并进行跳转.
指令格式: lookupswitch padbytes defaultbytes npairsbytes matchoffsetbytes
padbytes:0~3 个填充字节, 以使得 defaultbytes 与方法起始地址 (方法内第一条指令的操作码所在的地址) 之间的距离是 4 的位数.
defaultbytes:32 位默认跳转地址
npairsbytes:32 位匹配键值对的数量 npairs
matchoffsetbytes:npairs 个键值对, 每一组键值对都包含了一个 int 类型值 match 以及一个有符号 32 位偏移量 offset.
由于 case 条件值是非连续的, 因此无法采用像 tableswitch 直接定位的方式, 必须对每个键值进行比较. 然而, JVM 规定, lookupswitch 的跳转表必须根据键值排序, 这样 (如采用二分查找) 会比线性扫描更有效率.
下面结合实例分析一下:
第 1 条指令的索引号为 0,lookupswitch 指令索引号为 1, 为了使 defaultbytes 与方法起始地址之间的距离是 4 的位数, 所以 defaultbytes 的开始索引号为 4.
defaultbytes,npairsbytes 分别占 4 个字节, 总共 8 个字节.
case 有 3 个条件, 共 3 个键值对(npairs 为 3). 由于每个键值对占 8 个字节(4 字节 match+4 字节 offset), 因此 matchoffsetbytes 共占 24 个字节.
所以, 紧跟在 lookupswitch 后面的下一条指令的索引号为 4+8+24=36, 对应于实例中的指令 "36: iconst_m1".
题图: http://codeforwin.org
参考
《The Java Virtual Machine Specification, Java SE 8 Edition》
《Java 虚拟机规范》(Java SE 8 版)
来源: https://juejin.im/post/5bba26425188255c6c625570