专栏首页一猿小讲程序员进阶系列:年少不懂爱家家,懂了已是猿中人。

程序员进阶系列:年少不懂爱家家,懂了已是猿中人。

正式分享之前,先回忆一下作者年少时的一次真实的面试囧途。

经验老道的面试官:

先问个简单的问题,i++ 与 ++i 有啥区别?

年少懵懂的攻城狮:

i++ 先把操作数加 1,然后把操作数放入表达式中运算;

++i 先把操作数放入表达式运算,然后把操作数加 1。

经验老道的面试官:

略微点点头,虽然面带微笑,不过感觉不太满意的样子... ...

时隔多年,回想起那个面试场景,忍不住要感叹:年少不懂i++(爱家家),如今懂了却已是老码农(双鬓白)。

相信大部分人都会这么教科书式的回答,但是能否从字节码角度再深入一点点呢?

1

准备:拿到字节码指令集文件

首先具备 Java 环境(能打开此文章,说明你肯定具备此环境)。

java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

能开发代码的工具(不强求IntelliJ IDEA),然后写出如下图 IPlus.java 就可以。

public class IPlus {
    public static void main(String[] args) {
        int i = 0;
        i = i++;
        System.out.println(i);
    }
}

编译 IPlus.java 源文件,生成对应的字节码文件。

接下来对 IPlus.class 文件进行反编译,当然推荐可以使用工具 ClassPy、JavaClassViewer、jclasslib 查看 class 文件结构,本次就用 jdk 自带的命令 javap 来查看 class 文件的结构,并把字节码指令集重定向输出到文件 iplus_javap.txt 中。

javap -v IPlus.class >> iplus_javap.txt

javap 是 Java class 文件分解器,可以反编译,也可以查看 java 编译器生成的字节码,用于分解 class 文件,可以解析出当前类对应的 code 区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。

Classfile /Users/yiyuanxiaojiangV5/IdeaProjects/IPlus.class
  Last modified 2020-8-27; size 516 bytes
  MD5 checksum a5279417c4e7cce8408ce9bb53c44205
  Compiled from "IPlus.java"
public class IPlus
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #24.#25        // java/io/PrintStream.println:(I)V
   #4 = Class              #26            // IPlus
   #5 = Class              #27            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LIPlus;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               i
  #18 = Utf8               I
  #19 = Utf8               SourceFile
  #20 = Utf8               IPlus.java
  #21 = NameAndType        #6:#7          // "<init>":()V
  #22 = Class              #28            // java/lang/System
  #23 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(I)V
  #26 = Utf8               IPlus
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (I)V
{
  public IPlus();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LIPlus;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: iinc          1, 1
         6: istore_1
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 7
        line 6: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
            2      13     1     i   I
}
SourceFile: "IPlus.java"

接下来跟随小猿的脚步,一起去分析字节码指令集文件 iplus_javap.txt,尝试彻底搞懂 i++。

2

解剖:字节码全局了解

一:Classfile 文件信息

Classfile /Users/yiyuanxiaojiangV5/IdeaProjects/IPlus.class //class文件的路径
  Last modified 2020-8-27; size 516 bytes //最后一次修改时间以及该class文件的大小
  MD5 checksum a5279417c4e7cce8408ce9bb53c44205 //该类的MD5值
  Compiled from "IPlus.java" //编译自源文件

这块感觉不用详细解释,仔细去看,应该都能懂。

第 1 行:class 文件的路径
第 2 行:最后一次修改时间;该 class 文件的大小。
第 3 行:MD5 checksum 值,例如下载文件的场景下会用于检查文件完整性,检测文件是否被恶意篡改。
第 4 行:编译自 IPlus.java 源文件。

二:类主体部分定义信息

public class IPlus //类名
  minor version: 0 //次版本号
  major version: 52 //主版本号,52 对应 JDK 1.8
  flags: ACC_PUBLIC, ACC_SUPER //该类的权限修饰符(访问标志)

重点关注第 2、3 两行,为什么要重点关注呢?业务开发中估计多数都遇到过 Unsupported major.minor version 的错误。其实就是通过高版本的 JDK 进行编译(例如 JDK 1.8),然后跑在低版本的 JDK 上(JDK 1.5),就会报版本不支持。

为了使用方便,特意整理一 JDK 各版本图,请拿走不谢。

三:常量池信息

Constant pool: // 常量池,#数字相当于是常量池里的一个索引
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V // 方法引用
   #2 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream; // 属性引用
   #3 = Methodref          #24.#25        // java/io/PrintStream.println:(I)V
   #4 = Class              #26            // IPlus // 类引用
   #5 = Class              #27            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               LIPlus;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               i
  #18 = Utf8               I
  #19 = Utf8               SourceFile
  #20 = Utf8               IPlus.java
  #21 = NameAndType        #6:#7          // "<init>":()V //返回值
  #22 = Class              #28            // java/lang/System
  #23 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(I)V
  #26 = Utf8               IPlus
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (I)V

#数字相当于是常量池里的一个索引,例如上面代码段里 #1 代表的是一个方法引用,并且该引用由 #5.#21 构成。

在 JVM 规范中定义了很多常量类型,汇总本次的遇到的几个。

四:构造方法信息

public IPlus();
    descriptor: ()V //方法描述符,这里的V表示void
    flags: ACC_PUBLIC //权限修饰符
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0 // aload_0 把this装载到了操作数栈中
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LIPlus;

descriptor:方法入参和返回描述; flags:访问权限控制符为 public; stack:方法对应栈帧中的操作数栈的深度为 1; locals:本地变量数量为 1; args_size:参数数量为 1; aload:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶; invokespecial:调用一个初始化方法; LineNumberTable、LocalVariableTable:前者代表行号表,是为调试器提供源码行号与字节码的映射关系;后者代码本地变量表,存放方法的局部变量信息,属于调试信息。

思考一:通过这段字节码信息,印证了一个准则:在没有显示声明构造的情形下,Java 会默认提供无参构造方法。

思考二:虽然是无参构造器,为什么 args_size 的值是 1 呢?是因为无参构造器和非静态方法调用会默认传入 this 变量参数,其中 aload_0 即表示的 this。

五:main 方法的信息

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: iinc          1, 1
         6: istore_1
         7: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 7
        line 6: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
            2      13     1     i   I

通过 descriptor 、flags 能直观的能够读懂 main 方法的入参,返回值以及访问修饰符;通过 LocalVariableTable 运行时候的局部变量表,能够看到 main 函数的 args 参数保存在了 LocalVariableTable 中。

3

解剖:字节码指令看看 i++ 的执行

为了便于理解,把本次用的字节码指令先列一下,大家结合着去读。

重点关注 main 方法中的如下指令(红色圈住部分)

结合字节码指令列表,把上面红色圈住部分解读一下,主要分两部分。

//第一步:int i = 0;
0: iconst_0 // 将常量 0 推送至栈顶。
1: istore_1 // 将栈顶的值保存到局部变量 1 中,i = 0。

//第二步:i = i++;
2: iload_1 // 从局部变量 1 中装载 int 类型值入栈,此时栈顶的值为 0。
3: iinc    1, 1 // 将第 1 个局部变量进行加 1 操作,此时局部变量 i = 1。
6: istore_1 // 取出栈顶元素 0 保存到局部变量 1 中,此时的值为 0。

为了更清晰,不妨贴一个字节码里的指令与源代码的一个对应关系图。

懂了 i++ 的执行原理,再去看 ++i 的执行原理,就很容易了,本次不带着刻意去分析,放一张图你就懂了(左侧是 i++ 的指令,右侧是 ++i 的指令)。

简单做个总结:i++ 会在本地局部变量中对数字做相加,但是并没有将值推至栈,那么再次从栈中便会拿到相加前的数值,保存到本地变量中;而 ++i 也会将本地局部变量中的数字做相加,但是将数据做了入栈操作,那么再次从栈中便会拿到相加后的数值,再次压入到本地变量中。

4

寄语写最后

本次,主要让大家从字节码角度看看 i++、++i 的执行原理,希望通过本次分享,大家对 Java 字节码不再恐惧,并且希望能够学以致用,尝试分析更多场景,从骨子里理解所以然。

好了,本次就谈到这里,一起聊技术、谈业务、喷架构,少走弯路,不踩大坑。会持续输出原创精彩分享,敬请期待!

本文分享自微信公众号 - 一猿小讲(yiyuanxiaojiangV5),作者:一猿小讲

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-08-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 万恶的产品经理是推动程序员技术进步的不竭动力

    万恶的 PM 是推动程序员技术不断进步的不竭动力。产品汪,你不仅仅是一只可爱的狗,你更是一只藏獒,因为我们程序员都是一群饥饿到边缘的草原狼。—题记 PM 与...

    非著名程序员
  • 万恶的PM是推动程序员技术不断进步的不竭动力

    ? 万恶的PM是推动程序员技术不断进步的不竭动力。产品汪,你不仅仅是一只可爱的狗,你更是一只藏獒,因为我们程序员都是一群饥饿到边缘的草原狼。—题记 PM与猿...

    非著名程序员
  • 程序员幽默:66条让你笑爆肚皮的程序员段子

    1、程序猿最烦两件事,第一件事是别人要他给自己的代码写文档,第二件呢?是别人的程序没有留下文档。

    一墨编程学习
  • 小心那个深夜秒回你的程序员!

    老九君
  • 程序员幽默:66条让你笑爆肚皮的程序员段子

    1、程序猿最烦两件事,第一件事是别人要他给自己的代码写文档,第二件呢?是别人的程序没有留下文档。 2、程序猿的读书历程:x 语言入门 —> x 语言应用实践 ...

    Java高级架构
  • 108个程序员的笑话,你都看得懂吗?

    1、程序猿最烦两件事,第一件事是别人要他给自己的代码写文档,第二件呢?是别人的程序没有留下文档。 2、程序猿的读书历程:x语言入门—>x语言应用实践—>x语言高...

    智能算法
  • 程序猿用 C语言实现一封 中文情书,代码很简单!【附源码】

    代码很简单,就是全部用宏定义进行替换,但是以为引用了中文,需要Unicode码的支持,能在VS2005及其以上版本编写调试 ↓↓↓

    小林C语言
  • 挑演员?选剧本?大数据说了到底算不算

    一部电影或一部电视剧,想要获得观众认可,除了拥有高颜值、高演技的演员外,很大程度上依靠的还是编剧精心的剧情设置。7月22日的“影视遇到大数据巅峰思享会”上,关于...

    数据猿
  • 程序猿们那些可选的职业发展路线

    时不时会有一些做开发的小伙伴向我咨询一些职业发展的问题,比如:该不该跳槽?遇到了职业天花板该怎么破?如何才能成为架构师?等等。这些问题,说白了,其实都是如何选择...

    Keegan小钢
  • 阿里P7高级架构师分享6年多的Java工作经验(想冲破瓶颈者必看)

    很多工作了五年左右的程序员每天已经习惯了机器般的写代码,如果是这样那么你永远只会是个基础程序员,因为你不能只会用,你要知道原理,不至于让你自己实现一个出来,但是...

    美的让人心动
  • 写给程序猿的把妹指南:概述篇

    每个女人,都是一套复杂的系统,只不过,这套系统不是由程序员创造的,而是由大自然进化而成的。大部分程序猿不太懂得如何泡妞,是因为你不熟悉女人这套系统,也没人教过你...

    Keegan小钢
  • Java程序员职业发展应该怎么规划

    做好职业规划可以少走很多弯路,对于Java程序员来说,同样如此。IT培训网小编汇总了适合Java程序员发展的职业规划,希望对大家有所帮助!

    java架构师
  • 一千个程序员的一千个码注,笑skr人!

    毕竟那些年,程序员在代码注释里面隐藏了太多的小秘密,了解了这些秘密,你也就走进了程序员的内心。

    老九君
  • Java程序员的成长之路

    本篇介绍的是大体思路,以及每个节点所需要学习的书籍内容,如果大家对详细的技术点有需要,欢迎留言,后续我在写一篇每个阶段需要学习掌握的技术点。

    良月柒
  • 增长黑客和程序员沟通指南:避开这8个坑爹瞬间

    其实,国内外增长专家认为,增长黑客更多的是一种思维(mindset),而非技术(technical)。写代码属于锦上添花的技能,而不是必要条件。

    纯洁的微笑
  • 独家 | 一文读懂Hadoop(三):Mapreduce

    随着全球经济的不断发展,大数据时代早已悄悄到来,而Hadoop又是大数据环境的基础,想入门大数据行业首先需要了解Hadoop的知识。2017年年初apache发...

    数据派THU
  • “钢铁直猿”专属,5·20硬核表白方式三连击

    一年一度的狗粮大会又要在这骚动的春夏之交“5·20”开始了,面对情人节、“5·20”、七夕以及各种各样的纪念日里,如何向自己的女朋友表达爱意?

    大数据文摘
  • 大佬专访盘点 | 我在大数据领域创业的那些事儿!

    2016年刚刚过去,这注定是被铭记的一年。在这一年里,数据猿采访了七十多位大数据领域专家、学者以及创业者。他们用理念和实践推动着中国大数据产业发展。 我们对这些...

    数据猿
  • 大佬专访盘点 | 我在大数据领域创业的那些事儿!

    2016年刚刚过去,这注定是被铭记的一年。在这一年里,数据猿采访了七十多位大数据领域专家、学者以及创业者。他们用理念和实践推动着中国大数据产业发展。 我们对这些...

    数据猿

扫码关注云+社区

领取腾讯云代金券