《深入理解Java虚拟机》 字节码指令

———— JDK 1.9
words: 7.3k    views:    time: 28min

Java虚拟机的指令由一个字节长度(0 ~ 255)、代表着某种特定操作含义的数字(操作码),以及其后跟随的零至多个参数(操作数)构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码,指令参数都存放在操作数栈中。

采用一个字节来代表操作码,也是为了尽可能获得短小精干的编译代码,这种追求尽可能小数据量、高传输效率的设计,也是由于Java语言设计之初,主要面向网络、智能家电的技术背景所决定的,并一直沿用至今。

如果不考虑异常处理的情况,那么Java虚拟机的解释器可以使用下面这段伪代码作为最基本的执行模型来理解,虽然很简单,但可以正确有效的工作

1
2
3
4
5
6
do{
自动计算PC寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if(字节码存在操作数)从字节码流中取出操作数;
执行操作码所定义的操作;
} while(字节码流长度 > 0)

1. 字节码指令表

下面看一下具体的255个字节码指令

字节码 助记符 指令含义
0x00 nop 什么都不做
0x01 aconst_null 将 null 压入栈顶
将常量加载到操作数栈:const系列,负责把简单的数值压入栈顶,不需要参数
0x02 iconst_m1 将int型 -1 压入栈顶
0x03 iconst_0 将int型 0 压入栈顶
0x04 iconst_1 将int型 1 压入栈顶
0x06 iconst_3 将int型 3 压入栈顶
0x07 iconst_4 将int型 4 压入栈顶
0x08 iconst_5 将int型 5 压入栈顶
0x09 lconst_0 将long型 0 压入栈顶
0x0a lconst_1 将long型 1 压入栈顶
0x0b fconst_0 将float型 0 压入栈顶
0x0c fconst_1 将float型 1 压入栈顶
0x0d fconst_2 将float型 2 压入栈顶
0x0e dconst_0 将double型 0 压入栈顶
0x0f dconst_1 将double型 1 压入栈顶
将常量加载到操作数栈:push系列,负责将一个整形值压入栈顶,带一个参数,表示要压入栈顶的值
0x10 bipush 将单字节的常量值 -128 ~ 127 压入栈顶
0x11 sipush 将一个短整型常量值 -32768 ~ 32767 压入栈顶
将常量加载到操作数栈:ldc系列,负责将常量池中的数值常量或String常量引用压入栈顶,带一个参数,表示常量在常量池中位置
0x12 ldc 将int、float、或String型常量值从常量池中压入栈顶
0x13 ldc_w 将int、float、或String型常量值从常量池中压入栈顶(宽索引)
0x14 ldc2_w 将long、或double型常量值从常量池中压入栈顶
将局部变量加载到操作数栈:load系列
对于变量的编号是对所有类型的局部变量进行的,对于非静态函数,第一个变量是this,对其操作是aload_0
另外,对于函数传入的参数,以及返回值也算本地变量,并且在进行编号时,优先编号传入参数,返回值在最后
0x15 iload 将指定的int型局部变量压入栈顶
0x16 lload 将指定的long型局部变量压入栈顶
0x17 fload 将指定的float型局部变量压入栈顶
0x18 dload 将指定的double型局部变量压入栈顶
0x19 aload 将指定的引用类型局部变量压入栈顶
0x1a iload_0 将第一个int型局部变量压入栈顶
0x1b iload_1 将第二个int型局部变量压入栈顶
0x1c iload_2 将第三个int型局部变量压入栈顶
0x1d iload_3 将第四个int型局部变量压入栈顶
0x1e lload_0 将第一个long型局部变量压入栈顶
0x1f lload_1 将第二个long型局部变量压入栈顶
0x20 lload_2 将第三个long型局部变量压入栈顶
0x21 lload_3 将第四个long型局部变量压入栈顶
0x22 fload_0 将第一个float型局部变量压入栈顶
0x23 fload_1 将第二个float型局部变量压入栈顶
0x24 fload_2 将第三个float型局部变量压入栈顶
0x25 fload_3 将第四个float型局部变量压入栈顶
0x26 dload_0 将第一个double型局部变量压入栈顶
0x27 dload_1 将第二个double型局部变量压入栈顶
0x28 dload_2 将第三个double型局部变量压入栈顶
0x29 dload_3 将第四个double型局部变量压入栈顶
0x2a aload_0 将第一个引用类型局部变量压入栈顶
0x2b aload_1 将第二个引用类型局部变量压入栈顶
0x2c aload_2 将第三个引用类型局部变量压入栈顶
0x2d aload_3 将第四个引用类型局部变量压入栈顶
0x2e iaload 将int型数组指定索引的值压入栈顶
0x2f laload 将long型数组指定索引的值压入栈顶
0x30 faload 将float型数组指定索引的值压入栈顶
0x31 daload 将double型数组指定索引的值压入栈顶
0x32 aaload 将引用型数组指定索引的值压入栈顶
0x33 baload 将boolean或byte型数组指定索引的值压入栈顶
0x34 caload 将char型数组指定索引的值压入栈顶
0x35 saload 将short型数组指定索引的值压入栈顶
将数值从操作数栈存储到局部变量中:store系列
0x36 istore 将栈顶int型数值存入指定局部变量
0x37 lstore 将栈顶long型数值存入指定局部变量
0x38 fstore 将栈顶float型数值存入指定局部变量
0x39 dstore 将栈顶double型数值存入指定局部变量
0x3a astore 将栈顶引用型数值存入指定局部变量
0x3b istore_0 将栈顶int型数值存入第一个局部变量
0x3c istore_1 将栈顶int型数值存入第二个局部变量
0x3d istore_2 将栈顶int型数值存入第三个局部变量
0x3e istore_3 将栈顶int型数值存入第四个局部变量
0x3f lstore_0 将栈顶long型数值存入第一个局部变量
0x40 lstore_1 将栈顶long型数值存入第二个局部变量
0x41 lstore_2 将栈顶long型数值存入第三个局部变量
0x42 lstore_3 将栈顶long型数值存入第四个局部变量
0x43 fstore_0 将栈顶float型数值存入第一个局部变量
0x44 fstore_1 将栈顶float型数值存入第二个局部变量
0x45 fstore_2 将栈顶float型数值存入第三个局部变量
0x46 fstore_3 将栈顶float型数值存入第四个局部变量
0x47 dstore_0 将栈顶double型数值存入第一个局部变量
0x48 dstore_1 将栈顶double型数值存入第二个局部变量
0x49 dstore_2 将栈顶double型数值存入第三个局部变量
0x4a dstore_3 将栈顶double型数值存入第四个局部变量
0x4b astore_0 将栈顶引用型数值存入第一个局部变量
0x4c astore_1 将栈顶引用型数值存入第二个局部变量
0x4d astore_2 将栈顶引用型数值存入第三个局部变量
0x4e astore_3 将栈顶引用型数值存入第四个局部变量
将数值从操作数栈存储到局部数组变量中:store系列(数组)
0x4f iastore 将栈顶int型数值存入指定数组的指定索引位置
0x50 lastore 将栈顶long型数值存入指定数组的指定索引位置
0x51 fastore 将栈顶float型数值存入指定数组的指定索引位置
0x52 dastore 将栈顶double型数值存入指定数组的指定索引位置
0x53 aastore 将栈顶引用型数值存入指定数组的指定索引位置
0x54 bastore 将栈顶boolean或byte型数值存入指定数组的指定索引位置
0x55 castore 将栈顶char型数值存入指定数组的指定索引位置
0x56 sastore 将栈顶short型数值存入指定数组的指定索引位置
栈顶操作,操作结果一般重新入栈
0x57 pop 将栈顶数值弹出 (不是long或double类型)
0x58 pop2 将栈顶的一个(long或double类型),或两个(不是long或double类型)数值弹出
0x59 dup 复制栈顶数值(不是long或double类型),并将复制值压入栈顶
0x5a dup_x1 复制栈顶数值(不是long或double类型),并将两个复制值压入栈顶
0x5b dup_x2 复制栈顶数值(不是long或double类型),并将三个(或两个)复制值压入栈顶
0x5c dup2 复制栈顶一个数值(long或double类型)或两个(不是long或double类型),并将复制值压入栈顶
0x5d dup2_x1 复制栈顶数值(long或double类型),并将两个复制值压入栈顶
0x5e dup2_x2 复制栈顶数值(long或double类型),并将三个(或两个)复制值压入栈顶
0x5f swap 将栈最顶端的两个数值互换(数值不能是long或double类型的)
运算指令
0x60 iadd 将栈顶两int型数值相加,并将结果压入栈顶
0x61 ladd 将栈顶两long型数值相加,并将结果压入栈顶
0x62 fadd 将栈顶两float型数值相加,并将结果压入栈顶
0x63 dadd 将栈顶两double型数值相加,并将结果压入栈顶
0x64 isub 将栈顶两int型数值相减,并将结果压入栈顶
0x65 lsub 将栈顶两long型数值相减,并将结果压入栈顶
0x66 fsub 将栈顶两float型数值相减,并将结果压入栈顶
0x67 dsub 将栈顶两double型数值相减,并将结果压入栈顶
0x68 imul 将栈顶两int型数值相乘,并将结果压入栈顶
0x69 lmul 将栈顶两long型数值相乘,并将结果压入栈顶
0x6a fmul 将栈顶两float型数值相乘,并将结果压入栈顶
0x6b dmul 将栈顶两double型数值相乘,并将结果压入栈顶
0x6c idiv 将栈顶两int型数值相除,并将结果压入栈顶
0x6d ldiv 将栈顶两long型数值相除,并将结果压入栈顶
0x6e fdiv 将栈顶两float型数值相除,并将结果压入栈顶
0x6f ddiv 将栈顶两double型数值相除,并将结果压入栈顶
0x70 irem 将栈顶两int型数值作取模运算,并将结果压入栈顶
0x71 lrem 将栈顶两long型数值作取模运算,并将结果压入栈顶
0x72 frem 将栈顶两float型数值作取模运算,并将结果压入栈顶
0x73 drem 将栈顶两double型数值作取模运算,并将结果压入栈顶
0x74 ineg 将栈顶int型数值取负,并将结果压入栈顶
0x75 lneg 将栈顶long型数值取负,并将结果压入栈顶
0x76 fneg 将栈顶float型数值取负,并将结果压入栈顶
0x77 dneg 将栈顶double型数值取负,并将结果压入栈顶
位运算指令
0x78 ishl 将int型数值左移指定位数,并将结果压入栈顶
0x79 lshl 将long型数值左移指定位数,并将结果压入栈顶
0x7a ishr 将int型数值右移指定位数,并将结果压入栈顶
0x7b lshr 将long型数值右移指定位数,并将结果压入栈顶
0x7c iushr 将int型数值右移(无符号)指定位数,并将结果压入栈顶
0x7d lushr 将long型数值右移(无符号)指定位数,并将结果压入栈顶
0x7e iand 将栈顶两int型数值作 位与运算 并将结果压入栈顶
0x7f land 将栈顶两long型数值作 位与运算 并将结果压入栈顶
0x80 ior 将栈顶两int型数值作 位或运算 并将结果压入栈顶
0x81 lor 将栈顶两long型数值作 位或运算 并将结果压入栈顶
0x82 ixor 将栈顶两int型数值作 位异或运算 并将结果压入栈顶
0x83 lxor 将栈顶两long型数值作 位异或运算 并将结果压入栈顶
0x84 iinc 将指定int型变量增加指定值(比如i++, i--, i+=2)
类型转换指令
0x85 i2l 将栈顶int型数值强制转换成long型数值,并将结果压入栈顶
0x86 i2f 将栈顶int型数值强制转换成float型数值,并将结果压入栈顶
0x87 i2d 将栈顶int型数值强制转换成double型数值,并将结果压入栈顶
0x88 l2i 将栈顶long型数值强制转换成int型数值,并将结果压入栈顶
0x89 l2f 将栈顶long型数值强制转换成float型数值,并将结果压入栈顶
0x8a l2d 将栈顶long型数值强制转换成double型数值,并将结果压入栈顶
0x8b f2i 将栈顶float型数值强制转换成int型数值,并将结果压入栈顶
0x8c f2l 将栈顶float型数值强制转换成long型数值,并将结果压入栈顶
0x8d f2d 将栈顶float型数值强制转换成double型数值,并将结果压入栈顶
0x8e d2i 将栈顶double型数值强制转换成int型数值,并将结果压入栈顶
0x8f d2l 将栈顶double型数值强制转换成long型数值,并将结果压入栈顶
0x90 d2f 将栈顶double型数值强制转换成float型数值,并将结果压入栈顶
0x91 i2b 将栈顶int型数值强制转换成byte型数值,并将结果压入栈顶
0x92 i2c 将栈顶int型数值强制转换成char型数值,并将结果压入栈顶
0x93 i2s 将栈顶int型数值强制转换成short型数值,并将结果压入栈顶
比较指令
0x94 lcmp 比较栈顶两long型数值大小,并将结果(1/0/-1)压入栈顶
0x95 fcmpl 比较栈顶两float型数值大小,并将结果(1/0/-1)压入栈顶;若其中一个值为NaN,则结果为-1
0x96 fcmpg 比较栈顶两float型数值大小,并将结果(1/0/-1)压入栈顶;若其中一个值为NaN,则结果为-1
0x97 dcmpl 比较栈顶两double型数值大小,并将结果(1/0/-1)压入栈顶;若其中一个值为NaN,则结果为-1
0x98 dcmpg 比较栈顶两double型数值大小,并将结果(1/0/-1)压入栈顶;若其中一个值为NaN,则结果为-1
控制跳转指令
0x99 ifeq 当栈顶int型数值等于0时跳转
0x9a ifne 当栈顶int型数值不等于0时跳转
0x9b iflt 当栈顶int型数值小于0时跳转
0x9c ifge 当栈顶int型数值大于等于0时跳转
0x9d ifgt 当栈顶int型数值大于0时跳转
0x9e ifle 当栈顶int型数值小于等于0时跳转
0x9f if_icmpeq 比较栈顶两int型数值大小,当结果等于0时跳转
0xa0 if_icmpne 比较栈顶两int型数值大小,当结果不等于0时跳转
0xa1 if_icmplt 比较栈顶两int型数值大小,当结果小于0时跳转
0xa2 if_icmpge 比较栈顶两int型数值大小,当结果大于等于0时跳转
0xa3 if_icmpgt 比较栈顶两int型数值大小,当结果大于0时跳转
0xa4 if_icmple 比较栈顶两int型数值大小,当结果小于等于0时跳转
0xa5 if_acmpeq 比较栈顶两引用型数值,当结果相等时跳转
0xa6 if_acmpne 比较栈顶两引用型数值,当结果不相等时跳转
0xa7 goto 无条件跳转
0xa8 jsr 跳转至指定16位的offset位置,并将jsr下一条指令地址压入栈顶
0xa9 ret 返回至局部变量指定的index的指令位置(一般与jsr, jsr_w联合使用)
0xaa tableswitch 用于switch条件跳转,case值连续(可变长度指令)
0xab lookupswitch 用于switch条件跳转,case值不连续(可变长度指令)
方法返回指令
0xac ireturn 从当前方法返回int
0xad lreturn 从当前方法返回long
0xae freturn 从当前方法返回float
0xaf dreturn 从当前方法返回double
0xb0 areturn 从当前方法返回对象引用
0xb1 return 从当前方法返回void
域(字段)操作指令
0xb2 getstatic 获取指定类的静态域,并将其值压入栈顶
0xb3 putstatic 用栈顶的值为指定的类的静态域赋值
0xb4 getfield 获取指定类的实例域,并将其值压入栈顶
0xb5 putfield 用栈顶的值为指定的类的实例域赋值
方法操作指令
0xb6 invokevirtual 调用实例方法
0xb7 invokespecial 调用超类构造方法,实例初始化方法,私有方法
0xb8 invokestatic 调用静态方法
0xb9 invokeinterface 调用接口方法
0xba invokedynamic JDK 7新增,用于动态语言支持,后面单独整理
方法操作指令
0xbb new 创建一个对象,并将其引用值压入栈顶
0xbc newarray 创建一个指定原始类型(如int、float、char)的数组,并将其引用值压入栈顶
0xbd anewarray 创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
0xbe arraylength 获得数组的长度值并压入栈顶
异常抛出指令
0xbf athrow 将栈顶的异常抛出
类型操作指令
0xc0 checkcast 检验类型转换,检验未通过将抛出ClassCastException
0xc1 instanceof 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
同步操作指令
0xc2 monitorenter 获得对象的锁,进入同步方法或同步块
0xc3 monitorexit 释放对象的锁,退出同步方法或同步块
其它
0xc4 wide 扩展局部变量的宽度
0xc5 multianewarray 创建指定类型和指定维度的多维数组(执行时,操作栈中必须包含各维度的长度值),并将引用压入栈顶
0xc6 ifnull 为null时跳转
0xc7 ifnonnull 不为null时跳转
0xc8 goto_w 无条件跳转(宽索引)
0xc9 jsr_w 跳转至指定32位的offset位置,并将jsr_w下一条指令地址压入栈顶

2. 字节码执行

Java常被认为是解释执行的语言,这在JDK 1.0的时代也许还比较确切。但当主流的JVM都包含了即时编译器之后,Class文件中的代码到底是会被解释执行还是编译执行就只有JVM自己知道了。再后来,Java也发展出了可以直接生成本地代码的编译器(如Jaotc、GCJ、ExcelsiorJET),另外,C/C++也出现了通过解释器执行的版本(如CINT)。此时,再讨论解释执行还是编译执行,需要先确定具体的谈论对象及版本才有意义。

大部分的程序代码在转换成物理机的目标代码,或者虚拟机能执行的指令集之前,都需要进过如下步骤:

对于一门具体语言的实现来说,词法、语法的分析以至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,比如C/C++语言。也可以把这些步骤包括执行引擎全部封装到一个黑匣子中,比如JavaScrip。

对于Java语言,Javac编译器完成了对程序代码进行词法分析、语法分析,并生成抽象语法树,以及遍历语法树生成线性的字节码指令流的过程。而对于字节码的解释则由JVM进行,因此,Java的编译器是一个半独立的实现。

javac编译器输出的字节码指令流,基本上是一种基于栈的指令集架构,字节码指令流里面的指令大部分都是零地址指令,它们依赖操作数栈进行工作。与之相对的是基于寄存器的执行集,比如最经典的X86二地址指令集,这些指令依赖寄存器进行工作。

基于栈的指令集主要优点在于可移植,因为寄存器是由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束。比如在32位80x86体系的处理器中提供了8个32位的寄存器,而ARMv6体系的处理器则提供了30个32位的通用寄存器。

主要缺点是理论上执行速度相对来说会稍慢一些,这也是为什么所有主流物理机的指令集都采用的寄存器架构。不过这要在有解释执行的前提下讨论才行,如果经过即时编译器输出成物理机上的汇编指令流,那就与虚拟机采用哪种指令集架构没有关系了。

3. 字节码示例

下面做一些简单代码的字节码分析,其实在了解了上面的操作码动作之后,结合代码以及stack栈深度并不难看出字节码的执行过程。但需要注意的是,这里的执行过程仅仅是一个概念模型,虚拟机最终会对执行过程做出一些列优化来提高性能,导致实际的执行过程与字节码中所描述的指令操作步骤差距非常大。

3.1. 关于this、参数、返回值的字节码操作

1
2
3
4
5
6
7
8
9
private int i;

public int getI() {
return i;
}

public void setI(int i) {
this.i = i;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public int getI();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0 // this入栈
1: getfield #18 // Field i:I // 获取栈顶this实例的字段i值,并重新入栈
4: ireturn // 返回int

public void setI(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0 // this入栈
1: iload_1 // 将传入参数i值入栈
2: putfield #18 // Field i:I // 获取栈顶值,并给实例i字段赋值
5: return // 方法返回

3.2. 关于数组创建和赋值

1
2
int moneys[] = new int[5];
moneys[1] = 100;
1
2
3
4
5
6
7
8
9
10
Code:
stack=3, locals=2, args_size=1
0: iconst_5 // 常量5入栈
1: newarray int // 获取栈顶值,新建int型数组,并重新入栈
3: astore_1 // 将栈顶值存入局部引用变量2(moneys)
4: aload_1 // 将局部引用变量2(moneys)的值入栈
5: iconst_1 // 常量1入栈
6: bipush 100 // 常量100入栈
8: iastore // 获取栈顶3个值(值100,索引1,数组引用),然后给数组赋值
9: return // 方法返回

3.3. 简单的运算

1
2
3
4
5
int d=10;
d = d >> 1;
d++;
d--;
d += 2;
1
2
3
4
5
6
7
8
9
10
11
12
Code:
stack=2, locals=2, args_size=1
0: bipush 10 // 常量10入栈
2: istore_1 // 将栈顶值存入局部变量2
3: iload_1 // 将局部变量2入栈
4: iconst_1 // 常量1入栈
5: ishr // 获取栈顶2个值(值10,移位数1),进行右移操作,结果重新入栈
6: istore_1 // 将栈顶值存入局部变量2
7: iinc 1, 1 // 将第2个变量加1
10: iinc 1, -1 // 将第2个变量减1
13: iinc 1, 2 // 将第2个变量加2
16: return // 方法返回

3.4. 条件判断

1
2
3
4
5
6
7
8
9
10
int a = 11;
int b = 10;
boolean result = a > b;

if(result){
a += 2;
}else{
a--;
}
return result;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Code:
stack=2, locals=4, args_size=1
0: bipush 11 // 常量11入栈
2: istore_1 // 将栈顶值存入局部变量2
3: bipush 10 // 常量10入栈
5: istore_2 // 将栈顶值存入局部变量3
6: iload_1 // 将局部变量2入栈
7: iload_2 // 将局部变量3入栈
8: if_icmple 15 // 获取栈顶两个值并比较,如果小于等于0,就跳转至15
11: iconst_1 // 常量1入栈
12: goto 16 // 跳转至16
15: iconst_0 // 常量0入栈
16: istore_3 // 将栈顶值存入变量4(result)
17: iload_3 // 将局部变量4入栈
18: ifeq 27 // 如果等于0,就跳转至27
21: iinc 1, 2 // 将第2个变量加2
24: goto 30 // 跳转至30
27: iinc 1, -1 // 将第2个变量减2
30: iload_3 // 将局部变量4入栈
31: ireturn // 返回结果result

3.5. 方法调用(值传递)、new操作(dup)

对于方法调用,会涉及到一个参数传递的问题,那么Java到底是值传递还是引用传递。其实可以将引用也认为是一个值(对象的地址值),这样就可以统一认为Java的方法调用都是传值,区别只是传的是值本身,还是对象的地址值。

如果传入的实参就是值,那么无论方法中形参怎么变化,对实参都没有影响;而如果实参传的是地址值,即引用,那么方法中可以通过形参的地址值对对象进行访问和修改,并在实参中得到体现,但是,如果方法中的操作改动了形参中的地址值(比如赋值新对象地址),那么后续对这个引用的操作就与原本传入的实参没有关系了

比如下面示例,listCall中可以通过list引用对传入的ArrayList进行访问和修改,但如果对list引用进行了重新赋值,那么后面就无法再修改实参了。

1
2
3
4
5
6
7
8
9
10
public List<String> testList(){
List<String> list = new ArrayList<>();
listCall(list);
return list;
}

public void listCall(List<String> list){
list.add("a");
list = new ArrayList<>(); // 下面的操作与传入的list已经没有任何关系
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public java.util.List<java.lang.String> testList();
descriptor: ()Ljava/util/List;
flags: ACC_PUBLIC
Signature: #30 // ()Ljava/util/List<Ljava/lang/String;>;
Code:
stack=2, locals=2, args_size=1
0: new #31 // new ArrayList创建,引用入栈
3: dup // 复制栈顶引用,并将复制值入栈
4: invokespecial #33 // 获取栈顶引用并调用其构造器,这里消耗一个引用且不返回,所以上面要先dup一下
7: astore_1 // 将栈顶值存入变量2(list)
8: aload_0 // this入栈
9: aload_1 // 变量2(list)入栈
10: invokevirtual #34 // 获取栈顶两个值,并调用 this.listCall(list)
13: aload_1 // 变量2(list)入栈
14: areturn // 返回

public void listCall(java.util.List<java.lang.String>);
descriptor: (Ljava/util/List;)V
flags: ACC_PUBLIC
Signature: #42 // (Ljava/util/List<Ljava/lang/String;>;)V
Code:
stack=2, locals=2, args_size=2
0: aload_1 // 将局部变量2(形参list)入栈
1: ldc #16 // 常量“a”入栈
3: invokeinterface #43, 2 // 获取栈顶两个值,并调用 list.add("a")
8: pop // 弹出栈顶,
9: new #31 // new ArrayList创建,引用入栈
12: dup // 复制栈顶引用,并将复制值入栈
13: invokespecial #33 // 与上面类似
16: astore_1
17: return

3.6. 同步操作(字符串拼接)

很多代码规范中对于字符串的拼接操作有要求,即不允许在循环中进行字符串拼接。

通过下面的示例可以看出来,其实编译成字节码操作之后,会统一将字符串的拼接动作翻译成StringBuilder.append,这没什么问题,但如果在循环体中,会导致每次循环都进行一遍new - append - toString的操作,显然是不太明智的。

1
2
3
4
5
6
7
8
9
public String test(){
String str = "i";
synchronized(this){
for(int i = 0; i < 3; i++){
str = str + i;
}
}
return "a" + str + "b" + str + "c";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
Code:
stack=3, locals=4, args_size=1
0: ldc #16 // 常量“i”入栈
2: astore_1 // 将栈顶引用值存入变量2(str = "i")
3: aload_0 // this入栈

4: dup // 复制栈顶引用,并将复制值入栈
5: astore_2 // 将栈顶引用值存入变量3(锁)
6: monitorenter // 进入同步

7: iconst_0 // 常量0入栈
8: istore_3 // 将栈顶引用值存入变量4(i = 0)

9: goto 34 // 首先跳转34,比较是否(i < 0)

12: new #18 // 创建StringBuilder,并将引用入栈
15: dup // 复制栈顶引用,并将复制值入栈
16: aload_1 // 将变量2(str)入栈
17: invokestatic #20 // 获取栈顶值,调用 String.valueOf,结果重新入栈
20: invokespecial #26 // 获取栈顶两个值,调用构造器 StringBuilder(str)
23: iload_3 // 变量4(i)入栈
24: invokevirtual #29 // 获取栈顶两个值,调用 StringBuilder.append,结果入栈,即(str + i)
27: invokevirtual #33 // 获取栈顶值,调用 StringBuilder.toString,结果入栈
30: astore_1 // 栈顶值存入变量2(str),即str = str + i

31: iinc 3, 1 // 将变量4加1(i++)
34: iload_3 // 将变量4(i)入栈
35: iconst_3 // 常量3入栈
36: if_icmplt 12 // 获取栈顶两个值比较是否(i < 3),如果小于,就跳转至12

39: aload_2 // 将变量3(锁)入栈
40: monitorexit // 获取栈顶引用,退出同步

41: goto 47 // 异常情况处理
44: aload_2
45: monitorexit // 退出同步
46: athrow

47: new #18 // 类似的,创建StringBuilder,并将引用入栈
50: dup // 复制栈顶引用,并将复制值入栈
51: ldc #36 // 常量“a”入栈
53: invokespecial #26 // 调用构造器 StringBuilder("a")
56: aload_1 // 变量2(str)入栈
57: invokevirtual #38 // 调用 StringBuilder.append,即 "a" + str
60: ldc #41 // 常量“b”入栈
62: invokevirtual #38 // 调用 StringBuilder.append,即 "a" + str + "b"
65: aload_1 // 变量2(str)入栈
66: invokevirtual #38 // 调用 StringBuilder.append,即 "a" + str + "b" + str
69: ldc #43 // 常量“c”入栈
71: invokevirtual #38 // 调用 StringBuilder.append,即 "a" + str + "b" + str + "c"
74: invokevirtual #33 // 调用 StringBuilder.toString,结果入栈
77: areturn // 返回栈顶引用
Exception table:
from to target type
7 41 44 any // 异常表:7-41之间有任何异常,跳转至44
44 46 44 any


参考:

  1. Copyright ©《深入理解java虚拟机》
  2. https://www.zhihu.com/question/52749416
  3. https://www.zhihu.com/question/31203609
  4. https://hllvm-group.iteye.com/group/topic/25858