示意把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属性里。