JVM规范研读-2

来源:互联网 发布:报修系统 源码 编辑:程序博客网 时间:2024/05/16 06:05

接着上一篇点击打开链接


4 JVM虚拟机编译

指令的格式:

 <index> <opcode> [<operand1> [<operand2>...]] [<comment>]

  <index>是 code[]数组中的指令的操作码的索引,此处的 code[]数组就是存储当前方法的Java 虚拟机字节码的 Code 属性中的code[]数组。也可以认为<index>是相对于方法起始处的字节偏移量。<opcode>为指令的操作码的助记符号,<operandN>是指令的操作数,一条指令可以有 至多个操作数。<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()方法中,和 100 两个常量分别使用了两条不同的指令压入操作数栈。对于 采用了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 grain6 iconst_1 // Push int constant 17 isub // Subtract; push result8 iconst_m1 // Push int constant −19 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实例,但不包括doublelong类型的值。当使用的运行时常量池的项的数目过多时(多于256个,1个字节能表示的范围),需要使用ldc_w指令取代ldc指令来访问常量池。ldc2_w指令用于访问类型为doublelong的运行时常量池项。 

举个栗子

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 bipush2 istore_13 ldc #1 // Push int constant 1000000; a larger int// value uses ldc5 istore_26 lconst_1 // A tiny long value uses short, fast lconst_17 lstore_38 ldc2_w #6 // Push long 0xffffffff (that is, an int −1); any// long constant value can be pushed using ldc2_w11 lstore 513 ldc2_w #8 // Push double constant 2.200000; uncommon// double values are also pushed using ldc2_w16 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 bipush2 istore_13 ldc #1 // Push int constant 1000000; a larger int// value uses ldc5 istore_26 lconst_1 // A tiny long value uses short, fast lconst_17 lstore_38 ldc2_w #6 // Push long 0xffffffff (that is, an int −1); any// long constant value can be pushed using ldc2_w11 lstore 513 ldc2_w #8 // Push double constant 2.200000; uncommon// double values are also pushed using ldc2_w16 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 stack3 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_01 iload_12 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”压入到操作数栈中。传递给方法的参数,int1213随后入栈。当调用addTwo()方法时,Java虚拟机会创建一个新的栈帧,传递给addTwo()方法的参数作为新的帧的对应局部变量的初始值。即this和两个传递给的addTwo()方法的参数1213被作为addTwo()方法栈帧的第012个局部变量。 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 122 bipush 134 invokestatic #3 // Method Example.addTwoStatic(II)I7 ireturn

invokespecial 指令用于调用实例初始化方法(参见§3.8“使用类实例”),它也可以用来调用父类方法和私有方法。譬如下面例子中的 Near Far 两个类:

int add12and13() {    return addTwoStatic(12, 13);

}

Method int add12and13()0 bipush 122 bipush 134 invokestatic #3 // Method Example.addTwoStatic(II)I7 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_01 invokespecial #5 // Method Near.getIt()I4 ireturn

调用类Far的方法getItFar ()(调用父类方法)被编译为:

Method int getItFar()0 aload_01 invokespecial #4 // Method Near.getItNear()I4 ireturn

请注意,所有使用 invokespecial指令调用的方法都需要 this作为第一个参数,保存在第一个局部变量之中。 


编译器会先把一个方法所在的对象的引用压入操作数栈,方法参数则按顺序跟随这个对象之后入栈。编译器在生成invokevirtual指令时,也会生成这条指令所引用的描述符,这个描述符提供了方法参数和返回值的信息。作为方法解析时的一个特殊处理过程,一个用于调用java.lang.invoke.MethodHandleinvoke()或者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 variableMyObj 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 MyObj3 dup4 invokespecial #5 // Method MyObj.<init>()V7 astore_18 aload_09 aload_110 invokevirtual #4 // Method Example.silly(LMyObj;)LMyObj;13 areturn
Method MyObj silly(MyObj)0 aload_11 ifnull 64 aload_1
5 areturn6 aload_17 areturn


实例变量的访问:类实例的字段(实例变量)将使用getfield putfield 指令进行访问,假设i 是一个int型的实例变量,且方法 getIt()setIt()的定义如下:

void setIt(int value) {    i = value;

编译后代码如下:

Method void setIt(int)0 aload_01 iload_12 putfield #4 // Field Example.i I5 return
Method int getIt()0 aload_01 getfield #4 // Field Example.i I4 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 23 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虚拟机规范 — 第3JAVA虚拟机编译器

    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 20: 28 // If i is 0, continue at 281: 30 // If i is 1, continue at 302: 32 // If i is 2, continue at 32default:34 // Otherwise, continue at 3428 iconst_0 // i was 0; push int constant 0...29 ireturn // ...and return it30 iconst_1 // i was 1; push int constant 1...31 ireturn // ...and return it32 iconst_2 // i was 2; push int constant 2...33 ireturn // ...and return it34 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_11 lookupswitch 3:100: 360: 38100: 40default:4236 iconst_m137 ireturn38 iconst_039 ireturn40 iconst_141 ireturn42 iconst_m143 ireturn

4.10 操作数栈

public long nextIndex() {return index++;}private long index = 0;
Method long nextIndex()0 aload_0 // Push this1 dup // Make a copy of it2 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 this6 lconst_1 // Push long constant 17 ladd // The index value is incremented...8 putfield #4 // ...and the result stored back in the field11 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 throw4 new #1 // Create instance of TestExc7 dup // One reference goes to the constructor8 invokespecial #7 // Method TestExc.<init>()V11 athrow // Second reference is thrown12 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语句生成 jsrret指令了,而是改为在每个分支之后冗余代码的形式来实现 finally语句,所以在这节开头作者需要特别说明。在版本号为51.0(JDK 7Class文件)的Class文件中,甚至还明确禁止了指令流中出现jsrjsr_w指令。 

Oracle 1.0.2 版本 JDKjavac编译器生成的代码如下:

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 f1 dup // Duplicate it on the stack2 astore_2 // Store duplicate in local variable 23 monitorenter // Enter the monitor associated with f4 aload_0 // Holding the monitor, pass this and...5 invokevirtual #5 // ...call Example.doSomething()V8 aload_2 // Push local variable 2 (f)9 monitorexit // Exit the monitor associated with f10 goto 18 // Complete the method normally13 astore_3 // In case of any throw, end up here14 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 invoker18 return // Return in the normal caseException table:From To Target Type

4 10 13 any

13 16 13 any 

编译器必须确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter指令都必须有执行其对应 monitorexit指令,而无论这个方法是正常结束,还是异常结束。为了保证在方法异常完成时monitorenter monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit 指令。 



0 0
原创粉丝点击