前言

最近在看《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虚拟机》

JVM 字节码执行实例分析的更多相关文章

  1. JVM总结(五):JVM字节码执行引擎

    JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...

  2. 一夜搞懂 | JVM 字节码执行引擎

    前言 本文已经收录到我的 Github 个人博客,欢迎大佬们光临寒舍: 我的 GIthub 博客 学习导图 一.为什么要学习字节码执行引擎? 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一 ...

  3. 从JVM字节码执行看重载和重写

    Java 重写(Override)与重载(Overload) 重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变.即外壳不变,核心重写! 重写的 ...

  4. 深入理解JVM—字节码执行引擎

    原文地址:http://yhjhappy234.blog.163.com/blog/static/3163283220122204355694/ 前面我们不止一次的提到,Java是一种跨平台的语言,为 ...

  5. JVM字节码执行引擎和动态绑定原理

    1.执行引擎 所有Java虚拟机的执行引擎都是一致的: 输入的是字节码文件,处理过程就是解析过程,最后输出执行结果. 在整个过程不同的数据在不同的结构中进行处理. 2.栈帧 jvm进行方法调用和方法执 ...

  6. jvm 字节码执行 (一)方法调用

    “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上,而虚拟机的执行引擎是 由自己实现的,因此可以自行制定指令集 ...

  7. JVM字节码执行引擎

    一.概述 在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行(通过解释器执行)和编译器执行(通过即时编译器产生本地代码执行)两种选择,所有的Java虚拟机的执行引擎都是一致的:输 ...

  8. jvm 字节码执行 (二)动态类型支持与基于栈的字节码解释执行

    动态类型语言 动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期. 举例子解释“类型检查”,例如代码: obj.println("hello world"); 假 ...

  9. 图解JVM字节码执行引擎之栈帧结构

    一.执行引擎      “虚拟机”的概念是相对于“物理机”而言的,这两种“机器”都有执行代码的能力.物理机的执行引擎是直接建立在硬件处理器.物理寄存器.指令集和操作系统层面的:而“虚拟机”的执行引擎是 ...

随机推荐

  1. Oracle安装时忘记解锁scott用户的解决方案

    最近笔者开始学习Oracle,对于笔者同学安装过程出现各种问题而导致重做系统表示默哀. (1)问题1:64位操作系统安装完以后选择java.exe文件后.sqldeveloper闪退,原因是64位操作 ...

  2. stream_context_create解决file_get_contents超时处理

    stream_context_create作用:创建并返回一个文本数据流并应用各种选项,可用于fopen(),file_get_contents()等过程的超时设置.代理服务器.请求方式.头信息设置的 ...

  3. 虚拟机配置网络 vmware 【nat方式】

    先看下物理机和虚拟机网卡的设置: 虚拟机操作系统: 联网方式: 好了,现在进入正题,配置: 首相配置/etc/sysconfig/network-scripts/ifcfg-eth0,这里配置ip和d ...

  4. 多目标遗传算法 ------ NSGA-II (部分源码解析)状态报告 打印 report.c

    /* Routines for storing population data into files */ # include <stdio.h> # include <stdlib ...

  5. 国内外最全的asp.net开源项目 (转)

    最近一些项目开始用到CMS系统,最开始是研究JAVA的,无奈国内JAVA的CMS开源系统还是比较少,最多最成熟的还是PHP的,当然现在.NET的也不少了,这里做一下汇总备忘,留待学习研究. 国内系统: ...

  6. 需要插入子集的时候如何更新父级ID

    场景模拟: 我们需要在不同的新闻站点中采集新闻信息,  所以需要在数据库中保存一个新闻站点表(Site) 一个新闻表(News) 两表之间的关系是        Site(1)-News(N) 数据库 ...

  7. Ubuntu配置apache

    http://blog.csdn.net/ljchlx/article/details/21978431 http://www.2cto.com/os/201110/107283.html

  8. 【HDU 5184】 Brackets (卡特兰数)

    Brackets Problem Description We give the following inductive definition of a “regular brackets” sequ ...

  9. Eclipse下设置github开发环境

    1.按照github上的指南配置(http://help.github.com/win-set-up-git/)基础的git环境. 2.在github上创建一个Repository. 3.在Eclip ...

  10. linux内核--进程地址空间(三)

    引言:上篇博文中,我们简单的介绍了Linux虚拟存储器的概念及组成情况,下面来分析分析进程的创建和终结及跟进程地址空间的联系. 这里首先介绍一个比较重要的概念:存储器映射 在Linux系统中,通过将一 ...