《java虚拟机规范SE7》整理——第三章:为Java虚拟机编译

按照《java虚拟机规范SE7》章节顺序整理的笔记。

目录:

  1. 常量、局部变量的使用和控制结构
  2. 算术运算
  3. 访问运行时常量池
  4. 接收参数
  5. 方法调用
  6. 使用类实例
  7. 数组
  8. 编译switch语句
  9. 抛出异常和处理异常
  10. 同步

第三章:为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指令。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值