推荐学习

  • 肝了十天半月,献上纯手绘“Spring/Cloud/Boot/MVC”全家桶脑图
  • 一箭双雕!Alibaba架构师,纯手打Cloud+Boot微服务架构笔记
  • 最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

面试题

2018 年那会,我来酷划面试的时候,被问到了一道题,如下:

public int increment(){    int i = 0;    try{        return i++;    }catch (Exception e){        return i;    }finally {        System.out.println(i);        i++;    }}

问题是:

  1. 该方法的返回值是多少?(「答案是:0」)
  2. 是否会打印 i 的值?如果打印,打印出来的是多少?(「答案是:会打印,打印的是 1」)

题目很简单,第 2 问几乎都能答出来,但第 1 问,相信很多人看到后第一感觉是不确定:「到底是 0 还是 1?」

实际上,这是一类题,网上有很多和这道题类似的面试题,很多变种,如果靠背答案去死记硬背,很容易搞混。如果要从根本上弄明白这类题,就得从字节码和虚拟机栈的角度去解释了(事实上,我觉得能被问到这类题,最终的答案都是其次,面试官最终想考察的是求职者对 JVM 字节码部分的掌握程度)。

接下来,将从字节码的角度来解释这道面试题的答案。

在正式解释之前,先介绍一部分涉及到的基础知识,熟悉的同学,可以直接略过。

虚拟机栈

JVM 虚拟机是「基于操作数栈而不是基于寄存器」来执行的,每个线程都拥有一块私有的内存空间,这块内存空间被称作为虚拟机栈,虚拟机栈由一个个栈帧组成,每个方法对应一个栈帧。一个方法的调用过程,就对应一个栈帧的入栈和出栈。

每个栈帧的结构又可以细分为「局部变量表、操作数栈、动态链接、返回地址、其他附加信息」。今天主要介绍一下局部变量表和操作数栈。

局部变量表

局部变量表就是用来存放方法的参数、方法内部定义的局部变量等信息,局部变量表的容量以变量槽为最小存储单元,对于 byte、boolean、short、int、char、float、reference(引用类型)、returnAddress 类型的变量或者参数,只占用一个 slot,对于 long、double 类型的数据,占用 2 个 slot。

对于实例方法(未使用 static 修饰),局部变量表中索引为 0 的槽位存放的是 this 变量(这也是为什么在实例方法中能使用 this 关键字的原因),对于类方法(使用 static 修饰),局部变量表中则没有存放 this。方法的参数和内部定义的变量,则按照在代码中出现的先后顺序依次存储在局部变量表中。

需要说明的是,局部变量中的变量槽是可以重用的。什么意思呢?如果一个变量被定义在方法内部的一个代码块中,那么当代码块的语句执行结束后,这个变量所占用的变量槽是可以被后面的变量所重复利用的。例如如下示例:

public void slot(){    {        int a = 1;    }    int b = 0;}

在上面的示例中,a 变量所处的变量槽是可以被 b 重复使用的,该示例代码中,局部比变量表的大小为 2,局部变量表索引为 0 的地方存储的是 this 变量,索引为 1 的地方存储的是变量 a,当 a 所处的代码块结束后,索引为 1 的槽位存储的就是变量 b 了。

局部变量表的大小,在方法的编译时期就被确定了,并且被存储在方法的 Code 属性的 locals 数据项中(Code 指的是方法被编译成字节码文件后,用来描述方法的一个数据项)。

操作数栈

操作数栈是一个「先进后出」的栈结构,在方法的执行过程中,会使用字节码指令对数据进行加减乘除等操作,这些数据都是先被加载进操作数栈后(入栈)再进行操作的,最后再通过字节码指令写入到局部变量表中(出栈)。

操作数栈的大小也是在编译时期就确定下来的,操作数栈的大小被存储在方法的 Code 属性的 stack 数据项中。

字节码解释

知道了局部变量表和操作数栈的基础知识后,下面我们来看下文章开头的面试题如何使用字节码来解释。

首先我们将代码通过反编译工具进行反编译成我们能看的懂的字节码,反编译工具有 JDK 自带的 javap (命令行: 「javap -verbose XXX.class」),也可以使用 Idea 中的插件 jclasslib(强烈推荐,比较人性化,反编译结果更易于查看)。

反编译后,方法对应的字节码如下(为了节省篇幅,只展示出和本文相关的部分):

反编译结果

下面依次解释这些字节码的含义。

stack=2, locals=5, args_size=1

这一行表示的是操作数栈的大小为 2,局部变量表的大小为 5,参数的数量为 1(因为这是一个实例方法,所以 JVM 默认为每个方法传入一个 this 参数)。

再看下面这一行:

Exception table:     from    to  target type         2     7    19   Class java/lang/Exception         2     7    34   any        19    22    34   any        34    36    34   any

这一部分表示的是异常信息,当程序出现异常时,会按照这个异常表来执行程序。

该异常表的第一行表示的是如果在第 2-7 行出现了「java/lang/Exception」异常(这个异常就是我们在 catch 代码块中显示要捕获的异常),那么就跳转到 19 行开始执行。(注意:这里所说的行号,表示的字节码的行号,并不是我们在 Java 代码中行号)

如果 2-7 行出现了其他异常(超出了 java/lang/Exception 的范围),那么就会跳转到 34 行执行,这个 any 异常是编译器默认加上的。

如果在 19-22 行或者 34-36 行也出现了异常,那么也是跳转到 34 行执行。

LocalVariableTable:Start  Length  Slot  Name   Signature   20      14     2     e   Ljava/lang/Exception;    0      49     0  this   Lcom/tiantang/jvm/gc/HelloWorld;    2      47     1     i   I

这一部分信息描述的是局部变量中的信息,可以看到,槽位索引为 0 的地方存储的是 this,索引为 1 的地方存储的是变量 i,索引为 2 的地方存储的是异常信息 e。

而在前面我们看到的局部变量表的最大大小为 5,而这里只描述了 3 个变量,那还有 2 个变量是什么呢?别急,这一点放到后面再解释。

上面解释了一些基本信息,下面正式进入正题,下面依次解释每一行字节码的含义。

0: iconst_01: istore_12: iload_13: iinc          1, 16: istore_2
  1. 第 0 行表示使用字节码指令 iconst 从常量池中将数字 0 加载到操作数栈中;
  2. 第 1 行表示使用字节码指令 istore_1 将操作数栈顶的数据存储到局部变量表索引为 1 的槽位中,从前面的分析中,我们知道了索引为 1 的槽位就是变量 i,所以第 0 行和第 1 行字节码的作用就是:int i = 0。(istore_n 表示含义的是,将操作数栈顶的数据保存到局部变量槽位索引为 n 的变量上)
  3. 第 2 行字节码 iload_1 表示的是将局部变量表槽位索引为 1 的变量加载到操作数栈中,也就是将变量 i 的值加载到操作数栈顶,由于 i 的值为 0,因此执行完该行字节码之后,操作数栈顶的元素为 0。(iload_n 表示的含义是,将局部变量槽位索引为 n 的变量加载进操作数栈顶)
  4. 第 3 行字节码 「iinc 1,1」表示的是将槽位索引为 1 的变量的值加 1(iinc 字节码后面的跟了两个数字,第一个数字表示的是槽位索引,第二个数字表示自增的值),因此这一行的意思就是将局部变量表中的变量 i 的值加 1,那么执行完这一行以后,局部变量中 i 的值为 1。
  5. 第 6 行字节码 istore_2 表示的是将操作数栈顶的元素保存到局部变量表槽位索引为 2 的变量中,此时操作数栈顶的值是 0(第 2 行字节码 iload_1 加载的结果),因此索引为 2 的槽位上的存储的值为 0。「记住这个地方,后面会用到」。注意:此时局部变量表索引为 2 的槽位并不是代表 Exception e,后面才会代表。

「总结起来,第 0 - 6 行字节码的作用就是将变量 i 的值先赋值为 0,然后再累加,将其变为 1,同时还在索引为 2 的地方存储了一个元素,值为 0」。等价于我们代码中的:

int i = 0;try {    i++;    // 还没有return}

有人可能会奇怪,为什么字节码的行号一下子从第 3 行变成了第 6 行?这是因为字节码指令 iinc 可以拆分为 3 个步骤:iload、iadd、istore,这 3 个步骤的含义是:先将数据从局部变量表加载进操作数栈,然后将数据进行累加操作,最后再将操作数栈顶的数据存回局部变量,因此占据了 3 行。

接着继续往下看:

7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;10: iload_111: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V14: iinc          1, 117: iload_218: ireturn
  1. 第 7 行字节码表示的是调用 System.out 获取到 PrintStream 对象,同样这一行可以细分为几个步骤,因此下一行字节码的行号直接变成了第 10 行;
  2. 第 10 行字节码 iload_1 表示将槽位索引为 1 的数据加载进操作数栈,也就是将 i 的值加载到操作数栈顶,此时 i 的值为 1;
  3. 第 11 行字节码表示调用 PrintStream 的 println()方法,打印操作数栈顶的元素,也就是打印出 1。因此第 7-11 行字节码的作用就是等价于:
System.out.println(i);
  1. 第 14 行字节码又是「iinc 1,1」,前面已经介绍了,它的作用就是将局部变量表槽位索引为 1 的元素的值加 1,也就是将变量 i 的值加 1,那么 i 的值就变成了 2;
  2. 第 17 行字节码表示从局部变量表索引为 2 的槽位上,将数据加载进操作数栈,此时槽位上存储的值为 0(「文章前面已经强调过了为什么是 0」),因此操作数栈顶此时的值就变为了 0。
  3. 第 18 行字节码 ireturn 表示的是方法结束,并将操作数栈顶的元素返回出去,此时栈顶元素值为 0,因此方法最终的返回值为 0。

到这里,如果方法正常执行,不出现任何异常,那么就结束了,并返回 0。为什么是返回 0 而不是 1 呢?「从字节码中我们看到了,在 i 进行 i++之前,先将 i 的旧值 0 保存到了局部变量表中,然后再对 i 进行自增操作,最后在方法返回之前,先将保存的旧值 0 加载进操作数栈栈顶,然后再通过 ireturn 指令将操作数栈顶的数据返回。」

为什么会执行 finally 中的代码呢?从字节码层面看,「当 try 代码块中的代码 i++对应的字节码在执行完成后并没有立即出现 ireturn 指令,而是先出现了 finally 代码块中代码对应的字节码内容,然后才出现 ireturn 指令,这是编译器在编译阶段自动生成的,因此会执行 finally 块中的代码」

面试题已经解释清楚了,我们再继续往下看看后面的字节码内容,再看看 catch 代码块的执行逻辑。

如果在字节码的第 2 到 7 行出现了异常,且异常类型为「java/lang/Exception」或者是其子类型,那么就会跳转到第 19 行字节码开始执行。

19: astore_220: iload_121: istore_322: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;25: iload_126: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V29: iinc          1, 132: iload_333: ireturn
  1. 第 19 行的字节码指令 astore_2 表示将操作数栈顶的数据保存到局部变量表索引为 2 的槽位上,此时操作数栈顶的数据就是异常信息 e。注意,这里又出现了槽位索引为 2,还记得前面在代码正常执行过程中,也使用到了槽位 2,前面是将 i 的旧值保存到槽位 2 上,此时是将异常信息保存到槽位 2 上,从这一点看,「又再次验证了局部变量表上的槽位是可以重用的结论」
  2. 第 20 行字节码就是将槽位索引为 1 的变量值加载进操作数栈顶,即 i 的值加载进操作数栈顶;
  3. 第 21 行字节码就是将操作数栈顶的值保存到槽位索引为 3 的地方;
  4. 「第 22-29 行字节码对应的又是 finally 语句块中的内容,和前面一样」,就不解释了;
  5. 第 32 行字节码就是将槽位索引为 3 的数据加载进操作数栈顶;
  6. 第 33 行就是将操作数栈顶的数据返回,方法结束。

总结起来就是,如果出现了类型为「java/lang/Exception」或者是其子类型的异常,那么就会先保存下 i 的值,然后再执行 finally 代码块的中代码,最后再返回操作数栈顶的值,也就是 i 的旧值。

如果在字节码的 2-7 行、或者 19-22 行、或者 34-36 行出现了异常,也就是说出现了超出了「java/lang/Exception」类型的异常、或者在 catch、finally 代码块中又出现了了异常、或者在处理异常时又出现了异常,那么就会接着往下执行如下字节码:

34: astore        436: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;39: iload_140: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V43: iinc          1, 146: aload         448: athrow
  1. 第 34 行字节码的意思是,将异常信息保存到局部变量索引为 4 的槽位上;
  2. 「第 36-43 行字节码又是 finally 代码块中的内容」
  3. 第 46 行字节码的含义是,将局部变量表索引为 4 的数据加载进操作数栈顶,也就是将第 34 行保存的异常信息取出来;
  4. 第 48 行字节码的意思就是,将操作数栈顶的异常信息抛出,当前方法结束。

「总结来看,对于本文示例中,如果出现了 catch 代码块中无法捕获的异常,那么依旧会执行 finally 代码块中的内容,最后再将异常信息抛出,方法结束」

总结

本文花了一部分内容介绍了局部变量表和操作数栈,这两者是程序在执行过程中必不可少的内存结构,所有变量数据的变化以及字节码指令的操作对象,都离不开局部变量表和操作数栈。

本文花了很大的篇幅去介绍一道简单的面试题的答案,最终从字节码的角度解释了为什么返回值是 0,为什么 finally 语句块中的内容会被执行?

这是因为在执行 i++操作之前,先保存了 i 的旧值到局部变量的一个槽位中,然后再对 i 执行自增操作,最后将保存的旧值返回,即使在 finally 代码块中,对 i 的值进行了修改,也不会改变返回值。

而在 Java 代码的编译时期,编译器会将 finally 代码块中的代码所对应的字节码内容添加到 try、catch 以及其他异常处理的字节码内容中,因此无论方法是正常执行,还是出现能捕获的异常,亦或是无法捕获的异常,都会执行 finally 语句块中的代码。

掌握了 i++的原理,那么如果将题目换成++i,相信各位读者,在看过对应的字节码以后,应该就能明白返回值又是多少了。

public int increment() {    int i = 0;    try {        return ++i;    } catch (Exception e) {        return i;    } finally {        System.out.println(i);        i++;    }}

作者:天堂同志
链接:https://juejin.im/post/5f0b4f2ce51d4534b1306186

5码默认版块_用字节码解释try、catch、finally、i++、++i的执行结果?相关推荐

  1. 5码默认版块_速看!在阜阳,“5折乘公交”优惠来了

    为倡导市民绿色出行,号召广大市民尽可能多的选择绿色低碳出行方式,有效发挥阜阳交通公交主力军作用.阜阳公交携手银联继续推出便民服务举措,全年以"5折封顶1元"优惠活动为主,开展&qu ...

  2. 5码默认版块_短说社区论坛系统版块权限功能

    短说社区论坛系统近期开发了版块权限功能,版块权限功能与用户组功能关联,因此想要更好的使用版块权限功能,先了解短说用户组功能介绍.此贴详细介绍版块权限功能. 版块权限主要分为访问审核权限.版块访问权限. ...

  3. 美团热修复Robust源码庖丁解牛(第一篇字节码插桩)

    如果你想对java编译后的class文件做一些手脚的话,市面上有供你选择的asm.javassist.aspectJ(aop面向切面编程)等等,一般修改class文件的用途有你想统计一些东西,例如ap ...

  4. 二维码简介_二维码基本概念_二维码基本原理

    一.二维码简介_二维码基本概念_二维码基本原理 1.二维码又称二维条码,常见的二维码为QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Ba ...

  5. JVM与字节码——2进制流字节码解析

    为什么80%的码农都做不了架构师?>>>    字节码解析 结构 本位将详细介绍字节码的2进制结构和JVM解析2进制流的规范.规范对字节码有非常严格的结构要求,其结构可以用一个JSO ...

  6. 反射_获取字节码Class对象的三种方式

    * 获取Class对象的方式: 1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象     * 多用于配置文件,将类名定义在配置文件中.读取 ...

  7. 大端字节序码流中取出2字节_产生字节码

    大端字节序码流中取出2字节 在这篇文章中,我们将看到如何为我们的语言生成字节码. 到目前为止,我们已经看到了如何构建一种语言来表达我们想要的东西,如何验证该语言,如何为该语言构建编辑器,但实际上我们还 ...

  8. java中j 和 j啥区别_从字节码层次分析++j和j++的区别

    一.缘起 最近看到个面试题: int j = 0; for(int i = 0; i <100; i++) j = j++; System.out.println(j); 输出结果是0,如果换成 ...

  9. 【网上的都不靠谱?还是得改源码】用Javasisst的字节码插桩技术,彻底解决Gson转Map时,Int变成double问题...

    一.探究原由 首先申明一下,我们要解决的问题有两个: Json串转Map时,int变double问题 Json串转对象时,对象属性中的Map,int变double问题 然后,我们来了解一下,Gson实 ...

最新文章

  1. NDK JNI Android Studio开发与调试DEMO(三)(生成 .so 文件)
  2. 实战:使用 Mask-RCNN 的停车位检测
  3. 【学习笔记】HTTP通讯基础
  4. 论文浅尝 | 基于知识图谱 Embedding 的问答
  5. python测速程序_Python大数据分析学习.测试程序执行速度
  6. Spring boot : @PostConstruct @PreDestroy
  7. Linux 中安装软件报缺少共享库文件的错误
  8. 拓端tecdat|R语言用局部加权回归(Lowess)对logistic逻辑回归诊断和残差分析
  9. 几种常见模式识别算法整理和总结【转】
  10. 基于AVR单片机及无线收发模块的脉搏监测系统设计
  11. spring 定时器_细数那些使用过的定时器
  12. 决策树算法总结(下:CART决策树)
  13. 在思科路由器上配置DNS服务器
  14. 音乐flac怎么转为mp3?
  15. 软件测试黑马程序员课后答案_(完整版)软件测试技术基础课后习题答案
  16. linux下显卡不工作,Ubuntu 12.04下升级Nvidia后Bumblebee无法工作解决
  17. document.writeln
  18. html制作古诗网页早发白帝城,《早发白帝城》古诗词
  19. ZOJ Problem Set - 4043 Virtual Singers(2018acm 青岛赛区热身赛)
  20. Solver 配置详解

热门文章

  1. 《*** 法治思想学习纲要》学习辅导
  2. 快速去除照片的背景颜色和修改照片的背景颜色
  3. 专访富数科技吴海斌:隐私计算头部效应明显,2022年落地才是硬道理
  4. [软考2013计算机软件水平考试软件设计师考试大纲
  5. 详解BiLSTM及代码实现
  6. Xcode如何编写C++
  7. 可解释性神经网络——1.xNN
  8. 张艾迪(创始人):世界最高级文明信仰
  9. Chapter 3: Strings、Vectors And Arrays
  10. 计算机职称考试入户,揭秘!2020年考什么职称更容易入户广州?