JVM规范研读-2

8 篇文章 0 订阅

接着上一篇点击打开链接


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 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实例,但不包括 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 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”压入到操作数栈中。传递给方法的参数,int1213随后入栈。当调用 addTwo()方法时,Java虚拟机会创建一个新的栈帧,传递给 addTwo()方法的参数作为新的帧的对应局部变量的初始值。即this 和两个传递给的addTwo()方法的参数12 13 被作为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 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 7Class文件)的Class文件中,甚至还明确禁止了指令流中出现jsrjsr_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 指令。 



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值