Java虚拟机是为了支持Java语音而设计的,Oracle的JDK包括两部分内容:一部分是将Java源代码编译成Java虚拟机的指令集的编译器,另一部分是用于Java虚拟机的运行时环境。
2.1 示例的格式说明
本次的示例主要包括有源文件和Java虚拟机代码注解列表,其中Java虚拟机的代码注解列表是由Oracle的1.0.2版本的Jdk的javac编译器生成。Java虚拟机代码将使用Oracle的javap工具所生成的非正式的 虚拟机汇编语言的格式。
格式如下: <index> <opcode> [<operand1> [<operand2>...]] [<comment>]
其中<index>是Code[]数组总的指令的操作码的索引,此处的Code数组就是存储当前方法的Java虚拟机字节码的Code属性中的Code数组。<opcode> 为指令的操作码的助记符号,<operandN> 是指令的操作数,一条指令可以有0--N个操作数。<commnet>为行尾的语法注释。譬如
8 bipush 100 //push int constant 100
注释中的某些部分是有javap自动加入。每条指令前的<index>可以作为控制转移指令的跳转目标,譬如 goto 8 指令表示跳转到索引为8的指令上面继续执行。
2.2 常量、局部变量的使用和控制结构
Java虚拟机是基于栈架构设计的,它的大多数操作是从当前栈帧的操作数栈取出一个或多个操作数,或将结果压入操作数栈。每调用一个方法,都会创建一个新的栈帧,并创建对应方法所需的操作数栈和局部变量表。每条线程在运行时的任意时刻,都会包含若干个由不同方法嵌套调用而产生的栈帧,只有当前栈帧中的操作数栈才是活动的。Java虚拟机指令集合使用不同的字节码来区分不同的操作类型 ,用于操作各种类型的变量。比如 iconst_0、、istore_1、iinc 这几个指令是以i 开头,意思是 操作int类型的数据类型。比如如下代码: void test(){
int i;
for(i=0;i<100;i++){
//循环操作为空
}
}
进入cmd,在该文件目录下,执行 javac Demo.java。生成了.class文件后,再执行 javap -c Demo
对应的代码:
public void test();
Code:
0: iconst_0 //把int型 的值0压入操作数栈
1:istore_1 //把操作数的栈 赋值给i,即将值保存在局部变量中
2:iload_1 //把i的值取出
3:bipush 100 //取值100
5:if_icmpge 14 //把 0 和100进行比较,如果不满足,就 执行操作14,即 return.
8:iinc 1,1 //如果满足,i就自增1
11:goto 2 //自增1后 直接跳转到操作数2 iload_1 操作
14: return //方法结束,返回
在Java虚拟机中,缺乏对byte、char和short类型数据直接操作的支持所带来的问题并不大,因为这些类型的值都在编译过程中就自动被转换为int类型,唯一额外的代价是将他们的长度扩展至4字节。
2.3算术运算
Java虚拟机通常基于操作数栈来进行算术运算。算术运算使用到的操作数都是从操作数栈中弹出的,运算结果被压回操作数栈中。在内部运算的结果可以被当做操作数使用。
2.4访问运行时常量池
很多数值常量,以及对象,字段和方法,都是通过当前类的运行时常量池进行访问。类型为int、long、float和double的数据,以及表示String实例的引用类型数据的访问将由ldc、ldc_w和ldc2_w指令实现。其中 ldc和ldc_w指令用于访问运行时常量池中的对象,包括String实例,但不包括double和long类型。当使用的运行时常量池的项的数目过多时,多于256个,需要使用ldc_w指令取代ldc指令来访问常量池,ldc2_w指令用于访问类型为double和long类型的运行时常量池项。
参考示例:
void demo(){
int i=100;
int j=1000000;
long l1=1;
long l2=0xffffffff;
double d=2.2;
}
编译后的代码(先 javac ,然后javap -c ):
Method void demo()
0 bipush 100 //把int型 的值0压入操作数栈
2 istore_1 //把100 赋值给局部变量 i
3 ldc #1 //把 1000000 压入操作数栈
5 istore_2 //把1000000赋值给j
6 lconst_1 //将long 类型的1压入操作数栈,值比较小的 采用了short的用法
7 lstore_3 //将long类型的1 赋值给l1
8 ldc2_w #6 //将一个大的 long型数据压入操作数栈
11 lstore 5 //将上面这个数赋值给l2
13 ldc2_w //将2.2这个double类型的值压入操作数栈
16 dstore 7 //赋值给d
对比java代码和编译后的代码(javap命令),理解了几个常用的赋值,取值 命令。基本也是很容易理解。
2.5 接收参数
如果传递了n个参数给某个实例方法,则当前栈帧会按照约定的顺序接收这些参数,将他们保存为方法的第一个至第n个局部变量中。按照约定,实例方法需要传递一个自身实例的引用作为第0个局部变量。在Java语言中自身实例可以通过this关键字来访问。如果是静态方法,则不用传这个参数。
2.6方法调用
对普通实例方法调用是在运行时根据对象类型进行分派的,这类方法通过调用invokevirtual指令实现,每条invokevirtual指令都会带有一个表示索引的参数,运行时常量池在该索引处的项为某个方法的符号引用,这个符号引用可以提供方法所在对象的类型的内部二进制名称,方法名称和方法描述符。
2.7使用类实例
Java虚拟机类实例通过Java虚拟机new指令创建,在Java虚拟机层面,构造函数将以一个编译器提供的以<init>命名的方法出现。这个特殊的名字的方法也被称作实例初始化方法。一个类可以有多个构造函数,对应的也就会有多个实例初始化方法。一旦类实例被创建,那么这个实例包含的所有实例变量,除了在本身定义的以及父类中所定义的,都将赋予初始值。
2.8 其他类型
其他的类型 还包括 数组,switch语句,抛出异常和使用异常等。具体的详情可以参考java虚拟机规范。