第三章 Java虚拟机编译器

Avatar photo

示意把Java语言编写的源代码编译为Java虚拟机指令集的编译器。

3.1 示例的格式说明

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

javap生成的语句是非正式“虚拟机汇编语言”。<index>是方法起始处(指令的操作码集合地址)的字节偏移量,所以不一定是连续的,如果一行指令使用了两个字节,下一个index就会+2。

#数字 表示运行时常量池索引的操作数,注释(<comment>)中会有这个操作数的描述。

3.2 常量、局部变量和控制结构的使用

Java虚拟机是基于栈架构设计的。每调用一个方法就会创建一个新的栈帧。只有当前栈帧中的操作数才是活动的。

Java虚拟机指令集使用不同的字节码来区分不同的操作数类型。而且Java虚拟机经常利用操作码来隐式地包含某些操作数,例如指令iconst_0会把int类型的0值压入操作数栈,这样icount_0就不用专门为入栈操作保存直接操作数的值了。icoust_0替换成bipush 0也能获得正确结果。

局部变量表和操作数栈传递:istore_1,iload_1。这两个操作都默认对第一个局部变量进行操作。

3.3 算数运算

3.4 访问运行时常量池

ldc和ldc_w指令用于访问运行时常量池中的值。不包括double和long型,条目少时使用ldc,条目过多(多于256个,即1个字节能表示的范围)时,需要使用ldc_w。ldc2_w用于访问类型为double和long的运行时常量。

3.5 与控制结构有关的更多示例

void whileInt(){
   int i = 0;
   while(i<100){
      i++;
   }
}
Method void whileInt()
0 iconst_0
1 istore_1
2 goto 8
5 iinc 1 1
8 iload_1
9 bipush 100
11 if_icmplt 5
14 return

虚拟机会对代码进行优化,比如将判断指令放在循环底部,这样可以在每次运行中少运行一条Java虚拟机指令。

每个浮点类型数据都有两条比较指令:对于float类型是fcmpl和fcmpg,对于double是dcmpl和fcmpg。这些指令语义相似,仅仅在对待NaN变量时有所区别。(link 2.3.2 NaN时无序的
当操作数中有NaN时,fcmpl会把 -1 压入操作数栈,而fcmpg会压入 1在Java虚拟机指令集中,所有的算数比较指令都不会抛出异常。

3.6 接收参数

实例方法 传递一个指向该实例的引用作为方法的第0个局部变量。
类(static)方法不需要传递实例引用,所以他们不需要使用第0个局部变量来保存this关键字,而是会用它来保存方法的首个参数。

3.7 方法调用

普通实例方法通过调用invokevirtual指令实现,invokevirtual指令都会带有一个表示索引的参数,运行时常量池在该索引处的项为某个方法的符号引用,这个符号引用可以提供方法所在对象的类型的内部二进制名称、方法名称和方法描述符。(link 4.3.3
方法的返回由ireturn指令实现。

类(static)方法会通过invokestatic调用。

实力初始化、父类方法和私有方法通过invokespecial指令调用。

3.8 使用实力

Java虚拟机的实例通过虚拟机的new指令来创建。

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

类实例的字段(实例变量)将使用getfield和putfield指令进行访问。他们后面的操作数是常量池索引,不代表该字段在类实例中的偏移量。

3.9 数组

newarray指令用于创建元素类型为数值类型的数组。
multianewarry指令一次性创建多维数组。
arraylength指令能访问数组关联的长度属性。

3.10 编译switch语句

tableswitch可以高效地从索引表中确定case语句块的分支偏移量。适合比较密集的条件值。

1 tableswitch 0 to 2:
      0 : 28
      1 : 30
      2 : 32
      default : 34

lookupswitch指令的索引表项由int类型的健(来源于case语句块后面的数值)与对应的目标语句偏移量所构成。

1 lookupswitch 3:
              -100 : 36
                    0 : 38
                100 : 40
           default : 42

3.11 使用操作数栈

Java虚拟机为方便使用操作数栈,提供了大量不区分操作数类型的指令。???

3.12 抛出异常和处理异常

Java使用throw关键字抛出异常,编译后虚拟机使用athrow指令实现异常抛出。

在try-catch代码块中,try语句块仿佛没有生成任何指令。catch的代码生成在return之后,然后通过异常表(Exception table)调用。

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 retrun                           // End of try block; normal return
5 astore_1                        // Store thrown value in local var i
6 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

如果try-catch是嵌套关系的,那么会体现在异常表的From-To中。

*异常表中的From-To结构包含的代码块,含From偏移量,不包含To偏移量的指令。

3.13 编译finally语句块

* 只有编译器生成class文件版本小于50.0的情况下,才能使用jsr指令来编译finally语句块。

有四种方式让程序退出try语句:1️⃣正常退出;2️⃣通过return退出;3️⃣执行break或continue语句;4️⃣抛出异常。虚拟机会通过jsr指令跳转到finally代码块指令位置,执行完之后ret指令跳回。

3.14 同步

Java虚拟机中的同步(synchronization)是用monitor的进入和退出来实现的。
同步最多的地方可能是经synchronization所修饰的同步方法。同步方法并不是用monitorenter和monitorexit指令来实现的,而是由方法调用指令读取yun xing shi cgngn liang chi ang chi运行时常量池中方法的ACC_SYNCHRONIZED标志(link 2.11.10)来隐式实现的。【是不是非重量级锁实现?】
显示同方法通过monitorenter进入同步方法,monitorexit结束。编译器必须保证每条monitorenter都有对应的monitorexit指令得到执行。编译器会自动产生一个异常处理器,它可以处理任何异常,它的代码用来执行monitorexit指令。

3.15 注解

注解(annotation)将会在(link 4.7.16 – 4.7.22)描述class文件中的表示方式。给包声明的注解还需要遵循下面一些规则。
如果遇到某个添加了注解的包声明,而注解又必须能够在运行的时候得以访问,那么编译器要生成一份具有下列特征的class文件:

  • 如果是接口文件,ACC_INTERFACE 和 ACC_ABSTRACT 标志开启
  • 如果版本小于 50.0,就不设置ACC_SYNTHETIC标志;文件号大于50.0,则设置。
  • 该接口的访问权限是包级别的访问权限(JLS link 6.6.1)
  • 该接口的名称遵循package-name.package-info这一内部表示形式
  • 该接口没有父接口
  • 该接口所包含的成员,均是由《Java语言规范(Java SE 8 版)》所定义的成员(JSL link 9.2)
  • 在包声明层面所加的注释,保存于ClassFile结构attributes表中的RuntimeVisibleAnnotations属性及RuntimeInvisibleAnnotations属性里。