JVM 字节码执行实例分析

前言

最近在看《Java 虚拟机规范》和《深入理解JVM虚拟机》,对于字节码的执行有了进一步的了解。字节码就像是汇编语言,是 JVM 的指令集。下面我们先对 JVM 执行引擎做一下简单介绍,然后根据实例分析 JVM 字节码的执行过程。包括:

  1. for 循环字节码分析
  2. try-catch-finally 字节码分析

运行时栈帧结构

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

在编译程序员代码的时候,栈帧中局部变量表和操作数栈的大小已经确定了,并且写入到方法表中的 Code 属性中。

在活动线程中,只有位于栈顶的栈帧才是有效的, 称为当前栈帧,与这个栈帧关联的方法称为当前方法。执行引擎运行的所有字节码指令只对当前栈帧进行操作。

局部变量表

局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。局部变量表的容量以变量槽(slot)为最小单位,每个 slot 保证能放下 32 位内的数据类型。虚拟机通过索引定位的方式使用局部变量表,索引值从 0 开始。值得注意的是,对于实例方法,局部变量表中第 0 位索引的 slot 默认是 this引用;静态方法则不是。而且为了节约内存,slot 是可以重用的。

操作数栈

操作数栈的元素可以是任意的 Java 数据类型。当一个方法开始时,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈入栈操作。

实例分析

下面分析的字节码指令主要是对局部变量表和操作栈的读写。

for 循环字节码分析

void  spin() {
     int  i;
     for  (i = 0 ; i < 100 ; i++) {
         ; // Loop body is empty
     }
}

上面是一个空循环的代码,编译后的字节码如下:

Method void spin()
     0 iconst_0 //  Push int constant 0
     1 istore_1 //  Store into local  variable 1 (i=0)
     2 goto 8 //  First time  through don’t increment
     5 iinc 1 1 //  Increment local  variable 1 by 1 (i++)
     8 iload_1 //  Push local  variable 1 (i)
     9 bipush 100 //  Push int constant 100
     11 if_icmplt 5 //  Compare and loop if  less  than (i < 100)
     14 return  //  Return void when done

相信大家看到上面的代码都是一脸懵逼,即使有注释还是不知道字节码到底做了什么操作。下面我就图解每一条指令,帮助理解。上面的代码都是对局部变量表和操作数栈的操作,所以我们的关注点就在这两个区域上。(栈是自顶向下的)

0 iconst_0 // 把常量0放入栈
+--------+--------+
| local   | stack  |
+-----------------+
|        |   0    |
+-----------------+
|        |        |
+--------+--------+
 
1 istore_1 // 把栈顶的元素出栈,存到局部变量表索引为1的位置
+--------+--------+
| local   | stack  |
+-----------------+
|   0    |        |
+-----------------+
|        |        |
+--------+--------+
 
2 goto 8 // 跳转到第8条指令
 
8 iload_1 // 把局部变量表中索引为1的变量入栈
+--------+--------+
| local   | stack  |
+-----------------+
|   0    |   0    |
+-----------------+
|        |        |
+--------+--------+
 
9 bipush 100 // 把100入栈
+--------+--------+
| local   | stack  |
+-----------------+
|   0    |   0    |
+-----------------+
|        |  100   |
+--------+--------+
 
11 if_icmplt 5 // 出栈两个元素v1,v2,比较它们的值,当且仅当v1 < v2,跳转到指令5
+--------+--------+
| local   | stack  |
+-----------------+
|   0    |        |
+-----------------+
|        |        |
+--------+--------+
 
5 iinc 1 1 // 自增局部变量表中索引为1的值
+--------+--------+
| local   | stack  |
+-----------------+
|   1    |        |
+-----------------+
|        |        |
+--------+--------+
 
// 进行下次循环直到指令11不满足,到达指令14
14 return  // 清空栈,执行引擎把控制权交换给调用者。
+--------+--------+
| local   | stack  |
+-----------------+
|   100  |        |
+-----------------+
|        |        |
+--------+--------+

以上就是for循环字节码执行的过程。可以发现,所有指令都是围绕者局部变量表和操作数栈在操作。

解惑
指令iconst_0,iload_1的命名解读
第一个i代表这是对int数据类型进行的操作
const,load是操作码
0,1是隐含的操作数
上面的两个指令等价于iconst 0,iload 1
详细的字节码解释查阅《JVM 虚拟机规范》

try-catch-finally 字节码分析

static  int  inc(){
     int  x;
     try  {
         x = 1 ;
         return  x;
     } catch  (Exception e){
         x = 2 ;
         return  x;
     } finally  {
         x = 3 ;
     }
}

下面是它的字节码,这次我就不画图了,里面的命令跟上面的类似。

static  int  inc();
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 块中定义的 Exception e 赋值,存储在 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 //抛出栈顶的异常
   Exception table:
      from    to  target type
          0      4      8    Class java/lang/Exception
          0      4     17    any
          8     13     17    any

字节码中 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 虚拟机规范》、《深入理解JVM虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值