按照《java虚拟机规范SE7》章节顺序整理的笔记。
目录:
- 常量、局部变量的使用和控制结构
- 算术运算
- 访问运行时常量池
- 接收参数
- 方法调用
- 使用类实例
- 数组
- 编译switch语句
- 抛出异常和处理异常
- 同步
第三章:为java虚拟机编译
第三章讨论的主要是java虚拟机对java源文件的编译,这个过程体现在将java代码编译成字节码指令,也就是class文件的过程,而并没有包含将java代码编译成可由cpu执行的机器代码的过程(JIT)。
javap生成的反编译文件: javap生成的东西叫做,虚拟机生成的 “虚拟机汇编语言”,这也是本章讨论内容的最终结果。
它每条语句的格式如下:
<index> <opcode> [<operand1> [<operand2>...]] [<comment>]
index:代表的是code[]属性数组中操作指令的索引,code属性里面存储了方法的具体实现。
opcode:操作码指令,代表的java虚拟机的字节码指令集中的指令
operand:操作数,如果操作码有操作数,那么便填写到这里。
comment:注释,又编译器自动生成的对这行语句的一些解释。
语句中的 # 号: 在每一行中,在表示运行时常量池索引的操作数前,会井号(’#’)开头。
10 ldc #1 // Push float constant 100.0
<1>. 常量、局部变量的使用和控制结构
java虚拟机的指令集中有很多对常量的直接使用,它们隐藏在操作码中,如:iconst_< i >。
它不仅避免了操作数的读取和解析,还让编译后的代码更加简洁高效。
操作数与立即操作数:
操作数:即上面所介绍的跟在操作码后面的,将被操作码使用的参数。
立即操作数:在指令流中直接跟随在指令后面,而不是在操作数栈中的操作数称为立即操作数(也有直译为立即数或直接操作数)。
int的频繁使用: 在java虚拟机的指令集中,对int类型的操作最为频繁,因为如short,byte,char,boolean之类的原始类型都将在编译过程中自动转换成int类型的数据,这样才能使只有一个字节大小的指令集能够用。只是这样的代价是,这些小于int大小的类型,都将转化为四个字节。
<2>. 算术运算
Java虚拟机通常基于操作数栈来进行算术运算(只有iinc指令例外,它直接对局部变量进行自增操作)。
在内部运算时,中间运算(Arithmetic Subcomputations)的结果可以被当作操作数使用。
<3>. 访问运行时常量池
大多数操作的数值常量,都会从常量池中获取。如,对象,字段,方法。
对象的访问:ldc, ldc_w, ldc2_w。
整型的访问:使用bipush、sipush和iconst_指令进行访问。
某些浮点常量也可以编译进代码,使用fconst_和dconst_指令进行访问。
<4>. 接收参数
如果传递了n个参数给某个实例方法,则当前栈帧会按照约定的顺序接收这些参数,将它们保存为方法的第1个至第n个局部变量之中。
之所以从第1个开始,是因为,实例方法都会传递一个指向自身的this引用,并放在局部变量表的第0个位置。
然而对于static修饰的静态方法就没有这个参数,所以索引是从0开始。
<5>. 方法调用
方法有几个指令:invokevirtual, invokestatic, invokespecial。
每个方法调用时,都会生成方法的描述符。并且在方法调用时,方法的参数并不会进行类型转换,而是直接压入操作数栈中,紧跟在this之后。
invokevirtual:调用类的实例方法。
指令后面将跟一个以 # 开头的常量池索引,这个索引包含了索要调用对象所属的类,方法名,方法的描述符。描述符包含了方法的参数类型,参数数量,返回类型。
注:每个实例方法都会传入指向本真的引用this,并放入局部变量表的第0个位置。
invokestatic:调用类的静态方法。
注:与invokevirtual的区别就在,不会传入this引用。
invokespecial:调用类得实例初始化方法,或者父类方法,或者私有方法。
注:所有使用invokespecial指令调用的方法都需要this作为第一个参数,保存在第一个局部变量之中。
<6>. 使用类实例
new指令: 创建一个类实例。
类实例的创建过程: 类实例被创建,那么这个实例包含的所有实例变量,除了在本身定义的以及父类中所定义的,都将被赋予默认初始值,接着,新对象的实例初始化方法将会被调用。
这句话的意思大概是说:
- 首先生成的类对象,大概就是对内存的分配等初始化条件。
- 然后对除了本身定义 以及父类定义 之外的变量进行默认赋值,这个默认赋值差不多就是0,null这类的值。
- 最后才是调用实例初始化方法,按照代码中的值进行赋值。
类实例的字段的访问: 使用指令,getfield,putfield。
注:这些指令的操作数,都并非实际地址,而是常量池中的符号引用,通过这些符号引用最终来定位他们的实际地址。
<7>. 数组
创建数组分为:
- 创建原始类型的数组
- 创建引用类型的一维数组
- 创建多维数组
创建原始类型的数组:newarray 指令
例如: newarray int
创建引用类型的一维数组:anewarray 指令
例如:anewarray class #1
创建多维数组:multianewarray 指令
例如:multianewarray #1 dim #2
- 第一个参数为:运行时常量池索引
- 第二个参数为:创建数组的实际维度
<8>. 编译switch语句
指令有:tableswitch, lookupswitch
对于代码:
int chooseNear(int i) {
switch (i) {
case 0: return 0;
case 1: return 1;
case 2: return 2;
default: return -1;
}
}
使用tableswitch:
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
使用lookupswitch:
1 lookupswitch 3:
−100: 36
0: 38
100: 40
default:42
lookupswitch指令是把条件值与不同的key的进行比较,而tableswitch指令则只需要索引值进行一次范围检查。因此,在如果不需要考虑空间效率时,tableswitch指令相比lookupswitch指令有更高的执行效率。
<9>. 抛出异常和处理异常
抛出异常:athrow 指令
例:11 athrow // Second reference is thrown
处理异常:
在使用try-catch处理异常时,java虚拟机与反编译代码与没有这个try-catch没有区别,只是多出了一个新的属性:Exception table(异常表)
Exception table:
From To Target Type
0 4 5 Class TestExc
这个表会将catch语句中所要抓取的Exception记录下来。
如果有多个catch语句,也会依照代码的顺序严格记录下来。
异常可以嵌套:
对于嵌套的语句,区别只是在异常表上。
<10>. 同步
Java虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。
第一种:使用synchronized修饰的方法
实现:由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现的。
第二种:使用同步代码块
实现:使用monitorenter, monitorexit 实现。
注:为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令。