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

时间:2022-07-24
本文章向大家介绍程序员进阶系列:年少不懂爱家家,懂了已是猿中人。,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

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

经验老道的面试官:

先问个简单的问题,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 字节码不再恐惧,并且希望能够学以致用,尝试分析更多场景,从骨子里理解所以然。

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