如何从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数组。

结语

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


相关文章
|
5天前
|
Java
轻松上手Java字节码编辑:IDEA插件VisualClassBytes全方位解析
本插件VisualClassBytes可修改class字节码,包括class信息、字段信息、内部类,常量池和方法等。
35 6
|
1月前
|
存储 Java
【编程基础知识】 分析学生成绩:用Java二维数组存储与输出
本文介绍如何使用Java二维数组存储和处理多个学生的各科成绩,包括成绩的输入、存储及格式化输出,适合初学者实践Java基础知识。
66 1
|
14天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
31 2
|
17天前
|
Java 数据格式 索引
使用 Java 字节码工具检查类文件完整性的原理是什么
Java字节码工具通过解析和分析类文件的字节码,检查其结构和内容是否符合Java虚拟机规范,确保类文件的完整性和合法性,防止恶意代码或损坏的类文件影响程序运行。
|
17天前
|
Java API Maven
如何使用 Java 字节码工具检查类文件的完整性
本文介绍如何利用Java字节码工具来检测类文件的完整性和有效性,确保类文件未被篡改或损坏,适用于开发和维护阶段的代码质量控制。
|
15天前
|
Java 关系型数据库 数据库
面向对象设计原则在Java中的实现与案例分析
【10月更文挑战第25天】本文通过Java语言的具体实现和案例分析,详细介绍了面向对象设计的五大核心原则:单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖倒置原则。这些原则帮助开发者构建更加灵活、可维护和可扩展的系统,不仅适用于Java,也适用于其他面向对象编程语言。
10 2
|
1月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
18 1
|
1月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
38 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
1月前
|
安全 网络协议 Java
Java反序列化漏洞与URLDNS利用链分析
Java反序列化漏洞与URLDNS利用链分析
49 3
|
19天前
|
存储 Java 编译器
[Java]基本数据类型与引用类型赋值的底层分析
本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。
18 0