接着上一篇点击打开链接
4 JVM虚拟机编译
指令的格式:
<index> <opcode> [<operand1> [<operand2>...]] [<comment>]
<index>是 code[]数组中的指令的操作码的索引,此处的 code[]数组就是存储当前方法的Java 虚拟机字节码的 Code 属性中的code[]数组。也可以认为<index>是相对于方法起始处的字节偏移量。<opcode>为指令的操作码的助记符号,<operandN>是指令的操作数,一条指令可以有 0 至多个操作数。<comment>为行尾的语法注释,譬如:
8 bipush 100 // Push int constant 100
举个栗子
spin()是一个很简单的方法,它进行了 100 次空循环:
编译后代码如下:
void spin() {
int i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}}
Method void spin() 0 iconst_0 // Push int constant 0 1 istore_1 // Store into local variable 1 (i=0) 2 goto 8 // First time through don’t increment 5 iinc 1 1 // Increment local variable 1 by 1 (i++) 8 iload_1 // Push local variable 1 (i) 9 bipush 100 // Push int constant 100 11 if_icmplt 5 // Compare and loop if less than (i < 100)
14 return // Return void when done
spin()方法中,0 和 100 两个常量分别使用了两条不同的指令压入操作数栈。对于 0 采用了iconst_0 指令,它属于 iconst_<i>指令族。而对于 100 采用 bipush 指令,这条指令会获取它的立即操作数(Immediate Operand)1压入到操作数栈中。 spin()方法的第一个局部变量的传输过程由 istore_1 和 iload_1 指令完成,这两条指令都隐式指明了是对于第一个局部变量进行操作。istore_1 指令作用是从操作数栈中弹出一个 int 型的值,并保存在第一个局部变量中。iload_1 指令作用是将第一个局部变量的值压入操作数栈。
再举个栗子,不是整型
void dspin() {
double i;
for (i = 0.0; i < 100.0; i++) {
; // Loop body is empty
}}
Method void dspin() 0 dconst_0 // Push double constant 0.0 1 dstore_1 // Store into local variables 1 and 2 2 goto 9 // First time through don’t increment 5 dload_1 // Push local variables 1 and 2 6 dconst_1 // Push double constant 1.0 7 dadd // Add; there is no dinc instruction 8 dstore_1 // Store result in local variables 1 and 2 9 dload_1 // Push local variables 1 and 2 10 ldc2_w #4 // Push double constant 100.0 13 dcmpg // There is no if_dcmplt instruction 14 iflt 5 // Compare and loop if less than (i < 100.0)
17 return // Return void when done
double类型占两个slot
double doubleLocals(double d1, double d2) {
return d1 + d2;
}
Method double doubleLocals(double,double)
0 dload_1 // First argument in local variables 1 and 2
1 dload_3 // Second argument in local variables 3 and 4
2 dadd
3 dreturn
再举个栗子,将short提升为int
void sspin() {
short i;
for (i = 0; i < 100; i++) {
; // Loop body is empty
}}
Java 虚拟机编译时要将其他可安全转化为 int 类型的数据转换为 int 类型进行操作。在将short 类型值转换为 int 类型值时,可以保证 short 类型值操作后结果一定在 int 类型的精度范围之内,因此 sspin()的编译后代码如下:
Method void sspin()
0 iconst_0
1 istore_1
2 goto 10
5 iload_1 // The short is treated as though an int
6 iconst_1
7 iadd
8 i2s // Truncate int to short
9 istore_1
10 iload_1
11 bipush 100
13 if_icmplt 5
16 return
4.2 运算
int align2grain(int i, int grain) {
return ((i + grain-1) & ~(grain-1));
}
算术运算使用到的操作数都是从操作数栈中弹出的,运算结果被压回操作数栈中。在内部运算时,中间运算(Arithmetic Subcomputations)的结果可以被当作操作数使用。譬如~(grain-1)的值就是被这样使用的:
首先,grain−1的结果由第 2个局部变量和立即操作数 int数值 1的计算得出。参与运算的操作数从操作数栈中弹出,然后它们将被改变,最后再入栈到操作数栈之中。这里被改变是单个操作数的算术指令ixor运算的结果(因为~x == −1^x)。类似地,ixor指令的结果是接下来将作为iand 指令的操作数被使用。
整个方法的编译代码如下:
5 iload_2 // Push grain 6 iconst_1 // Push int constant 1 7 isub // Subtract; push result 8 iconst_m1 // Push int constant −1 9 ixor // Do XOR; push result
Method int align2grain(int,int)
0 iload_1
1 iload_2
2 iadd
3 iconst_1
4 isub
5 iload_2
6 iconst_1
7 isub
8 iconst_m1
9 ixor
10 iand
11 ireturn
4.3 访问常量池
ldc 和ldc_w 指令用于访问运行时常量池中的对象,包括 String实例,但不包括 double和long 类型的值。当使用的运行时常量池的项的数目过多时(多于256 个,1个字节能表示的范围),需要使用 ldc_w指令取代 ldc指令来访问常量池。ldc2_w指令用于访问类型为 double和long 的运行时常量池项。
void useManyNumeric() {
int i = 100;
int j = 1000000;
long l1 = 1;
long l2 = 0xffffffff;
double d = 2.2;
...do some calculations...
}
编译后代码如下:
Method void useManyNumeric() 0 bipush 100 // Push a small int with bipush 2 istore_1 3 ldc #1 // Push int constant 1000000; a larger int // value uses ldc 5 istore_2 6 lconst_1 // A tiny long value uses short, fast lconst_1 7 lstore_3 8 ldc2_w #6 // Push long 0xffffffff (that is, an int −1); any // long constant value can be pushed using ldc2_w 11 lstore 5 13 ldc2_w #8 // Push double constant 2.200000; uncommon // double values are also pushed using ldc2_w 16 dstore 7 ...do those calculations...
4.4 条件控制语句
void useManyNumeric() {
int i = 100;
int j = 1000000;
long l1 = 1;
long l2 = 0xffffffff;
double d = 2.2;
...do some calculations...
}
编译后代码如下:
Method void useManyNumeric() 0 bipush 100 // Push a small int with bipush 2 istore_1 3 ldc #1 // Push int constant 1000000; a larger int // value uses ldc 5 istore_2 6 lconst_1 // A tiny long value uses short, fast lconst_1 7 lstore_3 8 ldc2_w #6 // Push long 0xffffffff (that is, an int −1); any // long constant value can be pushed using ldc2_w 11 lstore 5 13 ldc2_w #8 // Push double constant 2.200000; uncommon // double values are also pushed using ldc2_w 16 dstore 7 ...do those calculations...
4.5 方法中的参数
int addTwo(int i, int j) {
return i + j;
}
Method int addTwo(int,int)
0 iload_1 // Push value of local variable 1 (i)
1 iload_2 // Push value of local variable 2 (j)
2 iadd // Add; leave int result on operand stack
3 ireturn // Return int result
实例方法需要传递一个自身实例的引用作为第 0个局部变量。在 Java语言中自身实例可以通过 this关键字来访问。
类(static)方法不需要传递实例引用,所以它们不需要使用第0 个局部变量来保存this关键字。如果addTwo()是类方法,那么接收的参数和之前示例相比略有不同:
static int addTwoStatic(int i, int j) {
return i + j;
}
Method int addTwoStatic(int,int)
0 iload_0
1 iload_1
2 iadd
3 ireturn
4.6 方法调用
对普通实例方法调用是在运行时根据对象类型进行分派的(相当于在 C++中所说的“虚方法”), 通过调用invokevirtual 指令实现,每条 invokevirtual指令都会带有一个表示索引的参数,运行时常量池在该索引处的项为某个方法的符号引用,这个符号引用可以提供方法所在对象的类型的内部二进制名称、方法名称和方法描述符 。
举个栗子
int add12and13() {
return addTwo(12, 13);
}
Method int add12and13()
0 aload_0 // Push local variable 0 (this)
1 bipush 12 // Push int constant 12
3 bipush 13 // Push int constant 13
5 invokevirtual #4 // Method Example.addtwo(II)I
8 ireturn // Return int on top of operand stack; it is the int result of addTwo()
第一步是将当前实例的自身引用“this”压入到操作数栈中。传递给方法的参数,int值 12和 13随后入栈。当调用 addTwo()方法时,Java虚拟机会创建一个新的栈帧,传递给 addTwo()方法的参数作为新的帧的对应局部变量的初始值。即this 和两个传递给的addTwo()方法的参数12 和13 被作为addTwo()方法栈帧的第0、1、2个局部变量。 当addTwo()方法执行结束、方法返回时,int型的返回值被压入方法调用者的栈帧的操作数栈,即 add12and13()方法的操作数栈中。这样addTwo()方法的返回值就被放置在调用者add12and13()方法的代码可以立刻使用到的地方。add12and13()方法的返回过程由add12and13()方法中的ireturn 指令实现。由于调用的addTwo()方法返回的int 值被压入当前操作数栈的栈顶,ireturn指令将会把当前操作数栈的栈顶值(此处就是 addTwo()的返回值)压入调用add12and13()方法的操作数栈。然后跳转至调用者调用方法的下一条指令继续执行,并将调用者的栈帧重新设为当前栈帧。
如果方法中,调用了static方法
int add12and13() {
return addTwoStatic(12, 13);
}
Method int add12and13()
0 bipush 12
2 bipush 13
4 invokestatic #3 // Method Example.addTwoStatic(II)I
7 ireturn
invokespecial 指令用于调用实例初始化方法(参见§3.8 “使用类实例”),它也可以用来调用父类方法和私有方法。譬如下面例子中的 Near 和 Far 两个类:
int add12and13() {
return addTwoStatic(12, 13);
}
Method int add12and13()
0 bipush 12
2 bipush 13
4 invokestatic #3 // Method Example.addTwoStatic(II)I
7 ireturn
class Near {
int it;
public int getItNear() {
return getIt();
}
private int getIt() {
return it;
}}
class Far extends Near {
int getItFar() {
return super.getItNear();
}}
调用类 Near 的方法 getItNear()(调用私有方法)被编译为:
Method int getItNear()
0 aload_0
1 invokespecial #5 // Method Near.getIt()I
4 ireturn
调用类Far的方法getItFar ()(调用父类方法)被编译为:
Method int getItFar()
0 aload_0
1 invokespecial #4 // Method Near.getItNear()I
4 ireturn
请注意,所有使用 invokespecial 指令调用的方法都需要 this 作为第一个参数,保存在第一个局部变量之中。
编译器会先把一个方法所在的对象的引用压入操作数栈,方法参数则按顺序跟随这个对象之后入栈。编译器在生成 invokevirtual 指令时,也会生成这条指令所引用的描述符,这个描述符提供了方法参数和返回值的信息。作为方法解析时的一个特殊处理过程,一个用于调用 java.lang.invoke.MethodHandle 的 invoke()或者invokeExact()方法的 invokevirtual 指令会提供一个方法描述符,这个方法描述符符合语法规则,并且在描述符中确定的类型信息将会被解析。
4.7 初始化init,cinit
Object create() {
return new Object();
}
Method java.lang.Object create()
0 new #1 // Class java.lang.Object
3 dup
4 invokespecial #4 // Method java.lang.Object.<init>()V
7 areturn
对象引用reference 类型也有它自己类型专有的 Java 虚拟机指令,譬如:
int i; // An instance variable
MyObj example() {
MyObj o = new MyObj();
return silly(o);
}
MyObj silly(MyObj o) {
if (o != null) {
return o;
} else {
return o;
}}
编译后代码如下:
Method MyObj example()
0 new #2 // Class MyObj
3 dup
4 invokespecial #5 // Method MyObj.<init>()V
7 astore_1
8 aload_0
9 aload_1
10 invokevirtual #4 // Method Example.silly(LMyObj;)LMyObj;
13 areturn
Method MyObj silly(MyObj)
0 aload_1
1 ifnull 6
4 aload_1
5 areturn
6 aload_1
7 areturn
实例变量的访问:类实例的字段(实例变量)将使用 getfield 和 putfield 指令进行访问,假设 i 是一个int 型的实例变量,且方法 getIt()和 setIt()的定义如下:
void setIt(int value) { i = value;编译后代码如下:
Method void setIt(int) 0 aload_0 1 iload_1 2 putfield #4 // Field Example.i I 5 return
Method int getIt() 0 aload_0 1 getfield #4 // Field Example.i I 4 ireturn
4.8 数组
void createBuffer() {
int buffer[];
int bufsz = 100;
int value = 12;
buffer = new int[bufsz];
buffer[10] = value;
value = buffer[11];
}
编译后代码如下:
Method void createBuffer()
0 bipush 100 // Push int constant 100 (bufsz)
2 istore_2 // Store bufsz in local variable 2
3 bipush 12 // Push int constant 12 (value)
5 istore_3 // Store value in local variable 3
6 iload_2 // Push bufsz...
7 newarray int // ...and create new array of int of that length9 astore_1 // Store new array in buffer
10 aload_1 // Push buffer
11 bipush 10 // Push int constant 10
13 iload_3 // Push value
14 iastore // Store value at buffer[10]
15 aload_1 // Push buffer
16 bipush 11 // Push int constant 11
18 iaload // Push value at buffer[11]...
19 istore_3 // ...and store it in value
20 return
anewarray 指令用于创建元素为引用类型的一维数组。譬如:
编译后代码如下:
void createThreadArray() {
Thread threads[];
int count = 10;
threads = new Thread[count];
threads[0] = new Thread();
}
Method void createThreadArray()
0 bipush 10 // Push int constant 10
2 istore_2 // Initialize count to that
3 iload_2 // Push count, used by anewarray
4 anewarray class #1 // Create new array of class Thread
7 astore_1 // Store new array in threads
8 aload_1 // Push value of threads
9 iconst_0 // Push int constant 0
10 new #1 // Create instance of class Thread
13 dup // Make duplicate reference...
14 invokespecial #5 // ...to pass to instance initialization method// Method java.lang.Thread.<init>()V
17 aastore // Store new Thread in array at 0
18 return
anewarray 指令也可以用于创建多维数组的第一维。不过我们也可以选择采用multianewarray 指令一次性创建多维数组。譬如三维数组:
int[][][] create3DArray() {
int grid[][][];
grid = new int[10][5][];
第 70 页 / 共 387 页
Java 虚拟机规范 — 第 3 章 JAVA 虚拟机编译器
return grid;
}
编译后代码如下:
Method int create3DArray()[][][]
0 bipush 10 // Push int 10 (dimension one)2 iconst_5 // Push int 5 (dimension two)3 multianewarray #1 dim #2 // Class [[[I, a three
// dimensional int array;
// only create first two
// dimensions
7 astore_1
8 aload_1
9 areturn
// Store new array...
// ...then prepare to return it
4.9 分支语句
int chooseNear(int i) {
switch (i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}}
编译后
Method int chooseNear(int) 0 iload_1 // Push local variable 1 (argument i) 1 tableswitch 0 to 2: // Valid indices are 0 through 2 0: 28 // If i is 0, continue at 28 1: 30 // If i is 1, continue at 30 2: 32 // If i is 2, continue at 32 default:34 // Otherwise, continue at 34 28 iconst_0 // i was 0; push int constant 0... 29 ireturn // ...and return it 30 iconst_1 // i was 1; push int constant 1... 31 ireturn // ...and return it 32 iconst_2 // i was 2; push int constant 2... 33 ireturn // ...and return it 34 iconst_m1 // otherwise push int constant –1... 35 ireturn // ...and return it
当 switch 语句中的 case 分支的条件值比较稀疏时,tableswitch 指令的空间使用率偏低。这种情况下将使用 lookupswitch 指令来替代。
int chooseFar(int i) {
switch (i) {
case -100: return -1;
case 0: return 0;
case 100: return 1;
default: return -1;
}}
编译后
Method 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
4.10 操作数栈
public long nextIndex() {
return index++;
}
private long index = 0;
Method long nextIndex()
0 aload_0 // Push this
1 dup // Make a copy of it
2 getfield #4 // One of the copies of this is consumed
// pushing long field index,
// above the original this
5 dup2_x1 // The long on top of the operand stack is
// inserted into the operand stack below the
// original this
6 lconst_1 // Push long constant 1
7 ladd // The index value is incremented...
8 putfield #4 // ...and the result stored back in the field
11 lreturn // The original value of index is left on
// top of the operand stack, ready to be returned
4.11 异常
void cantBeZero(int i) throws TestExc {
if (i == 0) {
throw new TestExc();
}}
Method void cantBeZero(int)
0 iload_1 // Push argument 1 (i)
1 ifne 12 // If i==0, allocate instance and throw
4 new #1 // Create instance of TestExc
7 dup // One reference goes to the constructor
8 invokespecial #7 // Method TestExc.<init>()V
11 athrow // Second reference is thrown
12 return // Never get here if we threw TestExc
try-catch语句
void catchOne() {
try {
tryItOut();
} catch (TestExc e) {
handleExc(e);
}
}
编译后代码如下:
Method void catchOne()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 return // End of try block; normal return
5 astore_1 // Store thrown value in local variable 16 aload_0 // Push this
7 aload_1 // Push thrown value
8 invokevirtual #5 // Invoke handler method:
// Example.handleExc(LTestExc;)V
11 return // Return after handling TestExc
Exception table:
From To Target Type
0 4 5 Class TestExc
异常表:
处理范围包括 from 但不包括 to 所表示的偏移量本身
4.12 Finally
void tryFinally() {
try {
tryItOut();
} finally {
wrapItUp();
}
}
很早之前(JDK 1.4.2 之前)的 Sun Javac 已经不再为 finally 语句生成 jsr 和 ret 指令了,而是改为在每个分支之后冗余代码的形式来实现 finally 语句,所以在这节开头作者需要特别说明。在版本号为51.0(JDK 7的Class文件)的Class文件中,甚至还明确禁止了指令流中出现jsr、jsr_w指令。
Oracle 的 1.0.2 版本 JDK 的 javac 编译器生成的代码如下:
Method void tryFinally()
0 aload_0 // Beginning of try block
1 invokevirtual #6 // Method Example.tryItOut()V
4 jsr 14 // Call finally block
7 return // End of try block
8 astore_1 // Beginning of handler for any throw
9 jsr 14 // Call finally block
12 aload_1 // Push thrown value
13 athrow // ...and rethrow the value to the invoker14 astore_2 // Beginning of finally block
15 aload_0 // Push this
16 invokevirtual #5 // Method Example.wrapItUp()V
19 ret 2 // Return from finally block
Exception table:
From To Target Type
0 48 any
“jsr 14”表示的意思是“调用程序子片段(Subroutine Call)”,这条指令使程序跳转至第 14 句的 finally 语句块
当 finally 语句块运行结束,使用“ret 2”指令将程序返回至 jsr 指令(即第 4句)的下一句继续执行。
4.13 同步语句
void onlyMe(Foo f) { synchronized(f) {
doSomething();
}
}
编译后代码如下:
Method void onlyMe(Foo)
0 aload_1 // Push f
1 dup // Duplicate it on the stack
2 astore_2 // Store duplicate in local variable 2
3 monitorenter // Enter the monitor associated with f
4 aload_0 // Holding the monitor, pass this and...
5 invokevirtual #5 // ...call Example.doSomething()V
8 aload_2 // Push local variable 2 (f)
9 monitorexit // Exit the monitor associated with f
10 goto 18 // Complete the method normally
13 astore_3 // In case of any throw, end up here
14 aload_2 // Push local variable 2 (f)
15 monitorexit // Be sure to exit the monitor!
16 aload_3 // Push thrown exception...
17 athrow // ...then rethrow the value to the invoker
18 return // Return in the normal case
Exception table:
From To Target Type
4 10 13 any
13 16 13 any
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都必须有执行其对应 monitorexit 指令,而无论这个方法是正常结束,还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。