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

结语

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


相关文章
|
1月前
|
缓存 JavaScript Java
常见java OOM异常分析排查思路分析
Java虚拟机(JVM)遇到内存不足时会抛出OutOfMemoryError(OOM)异常。常见OOM情况包括:1) **Java堆空间不足**:大量对象未被及时回收或内存泄漏;2) **线程栈空间不足**:递归过深或大量线程创建;3) **方法区溢出**:类信息过多,如CGLib代理类生成过多;4) **本机内存不足**:JNI调用消耗大量内存;5) **GC造成的内存不足**:频繁GC但效果不佳。解决方法包括调整JVM参数(如-Xmx、-Xss)、优化代码及使用高效垃圾回收器。
113 15
常见java OOM异常分析排查思路分析
|
8天前
|
安全 网络协议 Java
Java反序列化漏洞与URLDNS利用链分析
Java反序列化漏洞与URLDNS利用链分析
29 3
|
1月前
|
缓存 JavaScript Java
常见java OOM异常分析排查思路分析
Java虚拟机(JVM)遇到 OutOfMemoryError(OOM)表示内存资源不足。常见OOM情况包括:1) **Java堆空间不足**:内存被大量对象占用且未及时回收,或内存泄漏;解决方法包括调整JVM堆内存大小、优化代码及修复内存泄漏。2) **线程栈空间不足**:单线程栈帧过大或频繁创建线程;可通过优化代码或调整-Xss参数解决。3) **方法区溢出**:运行时生成大量类导致方法区满载;需调整元空间大小或优化类加载机制。4) **本机内存不足**:JNI调用或内存泄漏引起;需检查并优化本机代码。5) **GC造成的内存不足**:频繁GC但效果不佳;需优化JVM参数、代码及垃圾回收器
常见java OOM异常分析排查思路分析
|
25天前
|
Java
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
本文介绍了拼多多面试中的模拟拼团问题,通过使用 `CyclicBarrier` 实现了多人拼团成功后提交订单并支付的功能。与之前的 `CountDownLatch` 方法不同,`CyclicBarrier` 能够确保所有线程到达屏障点后继续执行,并且屏障可重复使用。文章详细解析了 `CyclicBarrier` 的核心原理及使用方法,并通过代码示例展示了其工作流程。最后,文章还提供了 `CyclicBarrier` 的源码分析,帮助读者深入理解其实现机制。
|
1月前
|
Arthas Java 测试技术
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
Java字节码文件、组成、详解、分析;常用工具,jclasslib插件、阿里arthas工具;如何定位线上问题;Java注解
Java字节码文件、组成,jclasslib插件、阿里arthas工具,Java注解
|
1月前
|
Java API 开发者
【Java字节码操控新篇章】JDK 22类文件API预览:解锁Java底层的无限可能!
【9月更文挑战第6天】JDK 22的类文件API为Java开发者们打开了一扇通往Java底层世界的大门。通过这个API,我们可以更加深入地理解Java程序的工作原理,实现更加灵活和强大的功能。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来!
|
1月前
|
Java API 开发者
【Java字节码的掌控者】JDK 22类文件API:解锁Java深层次的奥秘,赋能开发者无限可能!
【9月更文挑战第8天】JDK 22类文件API的引入,为Java开发者们打开了一扇通往Java字节码操控新世界的大门。通过这个API,我们可以更加深入地理解Java程序的底层行为,实现更加高效、可靠和创新的Java应用。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来,并积极探索类文件API带来的无限可能!
|
2月前
|
Dubbo Java 关系型数据库
Java SPI机制分析
文章深入分析了Java SPI机制,以JDBC为例,详细探讨了服务提供者接口的发现、加载过程,并提供了一个序列化服务的实战示例,展示了如何使用ServiceLoader进行服务发现和扩展。
25 3
|
1月前
|
安全 Java API
Java线程池原理与锁机制分析
综上所述,Java线程池和锁机制是并发编程中极其重要的两个部分。线程池主要用于管理线程的生命周期和执行并发任务,而锁机制则用于保障线程安全和防止数据的并发错误。它们深入地结合在一起,成为Java高效并发编程实践中的关键要素。
22 0
|
2月前
|
存储 消息中间件 监控
Java日志详解:日志级别,优先级、配置文件、常见日志管理系统ELK、日志收集分析
Java日志详解:日志级别,优先级、配置文件、常见日志管理系统、日志收集分析。日志级别从小到大的关系(优先级从低到高): ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF 低级别的会输出高级别的信息,高级别的不会输出低级别的信息