
课程咨询: 400-996-5531 / 投诉建议: 400-111-8989
认真做教育 专心促就业
在我们运行和调试Java程序的时候,经常会提到一个JVM的概念。JVM是Java程序运行的环境,而字节码就像是汇编语言,是JVM的指令集。下面达内java培训先对JVM执行引擎做一下简单介绍,然后根据实例分析JVM字节码的执行过程。
运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
在编译程序员代码的时候,栈帧中局部变量表和操作数栈的大小已经确定了,并且写入到方法表中的Code属性中。
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧关联的方法称为当前方法。执行引擎运行的所有字节码指令只对当前栈帧进行操作。
局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(slot)为小单位,每个slot保证能放下32位内的数据类型。虚拟机通过索引定位的方式使用局部变量表,索引值从0开始。值得注意的是,对于实例方法,局部变量表中0位索引的slot默认是this引用;静态方法则不是。而且为了节约内存,slot是可以重用的。
操作数栈
操作数栈的元素可以是任意的Java数据类型。当一个方法开始时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈入栈操作。
实例分析
下面分析的字节码指令主要是对局部变量表和操作栈的读写。
for循环字节码分析
voidspin(){
inti;
for(i=0;i<100;i++){
;//Loopbodyisempty
}
}
上面是一个空循环的代码,编译后的字节码如下:
Methodvoidspin()
0iconst_0//Pushintconstant0
1istore_1//Storeintolocalvariable1(i=0)
2goto8//Firsttimethroughdon’tincrement
5iinc11//Incrementlocalvariable1by1(i++)
8iload_1//Pushlocalvariable1(i)
9bipush100//Pushintconstant100
11if_icmplt5//Compareandloopiflessthan(i<100)
14return//Returnvoidwhendone
相信大家看到上面的代码都是一脸懵逼,即使有注释还是不知道字节码到底做了什么操作。下面图解每一条指令,帮助理解。上面的代码都是对局部变量表和操作数栈的操作,所以我们的关注点就在这两个区域上。(栈是自顶向下的)
0iconst_0//把常量0放入栈
+--------+--------+
|local|stack|
+-----------------+
||0|
+-----------------+
|||
+--------+--------+
1istore_1//把栈顶的元素出栈,存到局部变量表索引为1的位置
+--------+--------+
|local|stack|
+-----------------+
|0||
+-----------------+
|||
+--------+--------+
2goto8//跳转到8条指令
8iload_1//把局部变量表中索引为1的变量入栈
+--------+--------+
|local|stack|
+-----------------+
|0|0|
+-----------------+
|||
+--------+--------+
9bipush100//把100入栈
+--------+--------+
|local|stack|
+-----------------+
|0|0|
+-----------------+
||100|
+--------+--------+
11if_icmplt5//出栈两个元素v1,v2,比较它们的值,当且仅当v1
+--------+--------+
|local|stack|
+-----------------+
|0||
+-----------------+
|||
+--------+--------+
5iinc11//自增局部变量表中索引为1的值
+--------+--------+
|local|stack|
+-----------------+
|1||
+-----------------+
|||
+--------+--------+
//进行下次循环直到指令11不满足,到达指令14
14return//清空栈,执行引擎把控制权交换给调用者。
+--------+--------+
|local|stack|
+-----------------+
|100||
+-----------------+
|||
+--------+--------+
以上就是for循环字节码执行的过程。可以发现,所有指令都是围绕者局部变量表和操作数栈在操作。
解惑
指令iconst_0,iload_1的命名解读
一个i代表这是对int数据类型进行的操作
const,load是操作码
0,1是隐含的操作数
上面的两个指令等价于iconst0,iload1
详细的字节码解释查阅《JVM虚拟机规范》
try-catch-finally字节码分析
staticintinc(){
intx;
try{
x=1;
returnx;
}catch(Exceptione){
x=2;
returnx;
}finally{
x=3;
}
}
下面是它的字节码,这次不画图了,里面的命令跟上面的类似。
staticintinc();
descriptor:()I
flags:ACC_STATIC
Code:
stack=1,locals=4,args_size=0
0:iconst_1//try块中的x=1;
1:istore_0//保存栈顶元素到局部变量表中索引为0的slot中
2:iload_0//加载局部变量表中索引为0的值到栈中
3:istore_1//保存栈顶元素到局部变量表中索引为1的slot中
4:iconst_3//finally块中的x=3;
5:istore_0//保存栈顶元素到局部变量表中索引为0的slot中,x的值存在这里。
6:iload_1//加载局部变量表中索引为1的值到栈中
7:ireturn//返回栈顶元素,即x=1;正常情况下函数运行到这里就结束了,如果出现异常根据异常表跳转到指定的位置
8:astore_1//给catch块中定义的Exceptione赋值,存储在slot1中。
9:iconst_2//catch块中的x=2;
10:istore_0
11:iload_0
12:istore_2
13:iconst_3//finally块中的x=3;
14:istore_0
15:iload_2
16:ireturn//此时返回的是slot2中的值,即x=2
17:astore_3//如果出现不属于java.lang.Exception及其子类的异常,才会根据异常表中的规则跳转到这里。
18:iconst_3//finally块中的x=3;
19:istore_0
20:aload_3//将异常加载到栈顶,
21:athrow//抛出栈顶的异常
Exceptiontable:
fromtotargettype
048Classjava/lang/Exception
0417any
81317any
字节码中0~4行将整数1赋值为变量x,x存储在slot0中,并且将x的值拷贝一份放到slot1。如果没有出现异常,继续走到5~7行,将x赋值为3,然后读取slot1中的值到栈顶,后ireturn返回栈顶的值,方法结束。
如果出现异常,PC寄存器指针转到8行,8~16行所做的事情就是将2赋值给x,然后保存x的拷贝,后将x赋值为3。方法返回前将x的拷贝2读取到栈顶。
如果在0~4,8~13行中出现其他异常,则跳转到17行执行,先同样执行finally块中的x=3,后抛出异常,方法结束。
Java的异常处理是通过异常表的方式来决定代码执行的路径。而finally的实现是通过在每个路径的后加入finally块中的字节码实现的。达内java培训认为,熟练掌握这些,就能更好的进行java异常处理,希望对大家有所裨益。