如何从Java字节码角度分析问题|8月更文挑战

简介: 如何从Java字节码角度分析问题|8月更文挑战

前言

有一天逛知乎的时候,遇到了这样的问题:下面代码为什么i最后的结果是8?

public static void main(String[] args) {
  int i = 1;
  i += i += ++i + 2.6 + i;
}

很简单的两行代码,如果是你遇到这样的问题,你会怎样去把问题解释清楚?是利用Java运算符顺序将式子拆解,然后一步步运算,还是其他什么办法?

在思索一会儿之后,决定还是通过字节码指令来看看这两行代码是怎么运行的。

将两行代码拷贝到Test.java中,执行以下指令将Java源代码转换成字节码:

javac Test.java
javap -c Test.class

字节码输出结果如下: image.png 如果是之前对字节码没有了解的话,可以去搜一下字节码指令的资料,或者去《深入理解Java虚拟机》这本书去找附录b 字节码指令表

接下来翻译一下字节码:

public static void main(java.lang.String[]);
    Code:
       0: iconst_1  // 将1放入操作数栈顶
       1: istore_1  // 将操作数栈顶的i出栈并存放到局部变量表中slot中
       2: iload_1   // 从slot中取出i并放入操作数栈顶,此时栈内容为1
       3: iload_1   // 从slot取出i再次放入操作数栈顶,此时栈内容为1 1
       4: i2d       // 将操作数栈顶i的int转换为double类型,此时栈内容为1.0 1
       5: iinc      // ++i自增,此时slot中的i的值为2,记住,是2
       8: iload_1   // 从slot取出i放入栈顶,此时栈内容为2 1.0 1
       9: i2d       // 将栈顶的int类型转换为double类型
      10: ldc2_w    // 将2.6放入栈顶,此时栈内容为2.6 2.0 1.0 1
      13: dadd      // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 4.6 1.0 1 
      14: iload_1   // 将slot中的i放入栈顶,此时栈内容为 2 4.6 1.0 1 
      15: i2d       // 将栈顶的int类型转换为double类型,此时栈内容 2.0 4.6 1.0 1
      16: dadd      // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 6.6 1.0 1
      17: dadd      // 将栈顶的两个double相加,并把结果放入栈顶,此时栈内容为 7.6 1
      18: d2i       // 将栈顶的double转换为int类型7.6变成7,此时栈内容为7 1
      19: dup       // 复制栈顶数值并压栈,此时栈内容为 7 7 1
      20: istore_1  // 将i= i + (++i + 2.6 + i)的结果,i的值即7放入slot中,并出栈,此时栈内容7 1
      21: iadd      // 将栈顶两个int相加,此时栈内容为8
      22: istore_1  // i = i + (i + (++i + 2.6 + i))结果,即i的值即8放入slot,并出栈
      23: return    // 返回8

上面的字节码注释就是我的答案,一步一步的将运算步骤进行了拆解。

栈桢

上面提到的局部变量表和slot是什么?

这里就不得不提栈桢了。当我们执行一个方法的时候,虚拟机就会在线程私有的虚拟机栈栈顶创建一个栈桢来对应此方法。所以栈桢是方法调用和执行时的数据结构,包括局部变量表、操作数栈、动态连接等。

一个方法从开始调用到执行完成,对应了一个栈桢在虚拟机栈中入栈和出栈的过程。 image.png

局部变量表

局部变量表是用于存放方法参数和方法局部变量的空间,里面由一个个slot组成。代码在编译成字节码文件的时候,就可以确定局部变量表的大小。除了64位的long和double类型占用2个slot外,其他的数据类型占用1个slot。

操作数栈

在方法执行过程中,通过各种字节码指令往操作数栈中写入和读取数据,即入栈和出栈。数据的运算基于操作栈进行,例如iadd可以将栈顶的两个int类型进行加法运算。

动态连接

每个栈桢都会包含一个指向运行时常量池中该栈桢对应方法的符号引用,持有这个引用是为了支持方法调用过程的动态连接。将符号引用在运行期解析成直接引用的过程,叫做动态连接。

方法返回地址

方法会在以下两种情况进行退出:当遇到方法返回字节码指令时,根据方法逻辑决定是否会有返回值返回给调用者,然后正常退出方法;当遇到异常时,并且没有使用try来捕获异常,导致代码异常退出。

不论怎么样退出,都要返回到调用方法时的位置,栈桢中会保存方法返回时的一些信息,来恢复上层方法的执行状态。

扩展应用

最近网上比较流行的一个问题,为什么Integet类型的100 == 100返回true,200 == 200返回false?众所周知,==比较的是两个对象的地址,为什么两个对象的地址能一样?这里就让我们来探索一下:

源码如下:

public static void main(String[] args) {
        Integer a = 100;
        Integer b = 100;
        Integer c = 200;
        Integer d = 200;
        System.out.println(a == b);
        System.out.println(c == d);
    }

输出结果: image.png

字节码如下:

public static void main(java.lang.String[]);
    Code:
       0: bipush        100
       2: invokestatic  #2     // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: bipush        100
       8: invokestatic  #2    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      11: astore_2
      12: sipush        200
      15: invokestatic  #2    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      18: astore_3
      19: sipush        200
      22: invokestatic  #2    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
      25: astore        4
      27: getstatic     #3    // Field java/lang/System.out:Ljava/io/PrintStream;
      30: aload_1
      31: aload_2
      32: if_acmpne     39
      35: iconst_1
      36: goto          40
      39: iconst_0
      40: invokevirtual #4    // Method java/io/PrintStream.println:(Z)V
      43: getstatic     #3    // Field java/lang/System.out:Ljava/io/PrintStream;
      46: aload_3
      47: aload         4
      49: if_acmpne     56
      52: iconst_1
      53: goto          57
      56: iconst_0
      57: invokevirtual #4   // Method java/io/PrintStream.println:(Z)V
      60: return

从字节码中可以看到a、b、c、d赋值的时候都是通过invokestatic字节码指令调用了Integer.valueOf()方法。

但是不同的是,在给a、b赋值时候字节码指令是bipush,是将单字节的整型常量值(-128 - 127)压入操作数栈顶;给c、d赋值时候字节码指令是sipush,是将int类型的常量值压入操作数栈顶。

为什么同样是Integer类型,一个是1个字节,一个是4个字节呢?

那我们来探索一下Integer的valueOf()方法: image.png

这个方法调用了重载的valueOf(),代码如下: image.png

如上所示,这个IntegerCache是Integer的一个静态内部类,会对你初始化的Integer的值进行判断,当这个值在lowhigh之间,即-128 ~ 127,不会重新在堆中分配内存创建Integer对象,会直接从cache数组中返回一个Integer对象,所以a == b。

IntegerCache源码如下:

image.png

可以看出,在static静态块中通过for循环,初始化了cache数组。

结语

文章可能对栈桢描述的并没有那么详细,主要还是让大家大致了解一下栈桢基本的功能作用,普及一下字节码的作用。当我们对一些代码无法理解的时候,换个角度去理解可能会豁然开朗。


相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1517 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
492 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
448 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
313 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析