如何从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月前
|
Java Go 开发工具
【Java】(9)抽象类、接口、内部的运用与作用分析,枚举类型的使用
抽象类必须使用abstract修饰符来修饰,抽象方法也必须使用abstract修饰符来修饰,抽象方法不能有方法体。抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接 口、枚举)5种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类abstract static不能同时修饰一个方法。
184 1
|
1月前
|
存储 Java Go
【Java】(3)8种基本数据类型的分析、数据类型转换规则、转义字符的列举
牢记类型转换规则在脑海中将编译和运行两个阶段分开,这是两个不同的阶段,不要弄混!
168 2
|
2月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
3月前
|
安全 Java 编译器
new出来的对象,不一定在堆上?聊聊Java虚拟机的优化技术:逃逸分析
逃逸分析是一种静态程序分析技术,用于判断对象的可见性与生命周期。它帮助即时编译器优化内存使用、降低同步开销。根据对象是否逃逸出方法或线程,分析结果分为未逃逸、方法逃逸和线程逃逸三种。基于分析结果,编译器可进行同步锁消除、标量替换和栈上分配等优化,从而提升程序性能。尽管逃逸分析计算复杂度较高,但其在热点代码中的应用为Java虚拟机带来了显著的优化效果。
100 4
|
3月前
|
自然语言处理 前端开发 算法
Java编译器优化秘籍:字节码背后的IR魔法与常见技巧
编译器将源代码转换为机器码的过程中,会经历多个中间表达形式(IR)的转换与优化。前端生成高级IR(HIR),后端将其转为低级IR(LIR)并进行机器相关优化。Java编译流程包括源码到字节码、再由即时编译器转换为内部HIR(如SSA图)、优化后生成LIR,最终编译为机器码。常见优化技术包括常量折叠、值编号、死代码消除、公共子表达式消除等,旨在提升程序性能与执行效率。
124 0
|
3月前
|
机器学习/深度学习 安全 Java
Java 大视界 -- Java 大数据在智能金融反洗钱监测与交易异常分析中的应用(224)
本文探讨 Java 大数据在智能金融反洗钱监测与交易异常分析中的应用,介绍其在数据处理、机器学习建模、实战案例及安全隐私等方面的技术方案与挑战,展现 Java 在金融风控中的强大能力。
|
4月前
|
存储 Java 大数据
Java 大视界 -- Java 大数据在智能家居能源消耗模式分析与节能策略制定中的应用(198)
简介:本文探讨Java大数据技术在智能家居能源消耗分析与节能策略中的应用。通过数据采集、存储与智能分析,构建能耗模型,挖掘用电模式,制定设备调度策略,实现节能目标。结合实际案例,展示Java大数据在智能家居节能中的关键作用。
|
5月前
|
数据采集 搜索推荐 算法
Java 大视界 -- Java 大数据在智能教育学习社区用户互动分析与社区活跃度提升中的应用(274)
本文系统阐述 Java 大数据技术在智能教育学习社区中的深度应用,涵盖数据采集架构、核心分析算法、活跃度提升策略及前沿技术探索,为教育数字化转型提供完整技术解决方案。
|
5月前
|
Java 数据库连接 API
互联网大厂校招 JAVA 工程师笔试题解析及常见考点分析
本文深入解析互联网大厂校招Java工程师笔试题,涵盖基础知识(数据类型、流程控制)、面向对象编程(类与对象、继承与多态)、数据结构与算法(数组、链表、排序算法)、异常处理、集合框架、Java 8+新特性(Lambda表达式、Stream API)、多线程与并发、IO与NIO、数据库操作(JDBC、ORM框架MyBatis)及Spring框架基础(IoC、DI、AOP)。通过技术方案讲解与实例演示,助你掌握核心考点,提升解题能力。
218 2
|
传感器 分布式计算 安全
Java 大视界 -- Java 大数据在智能安防入侵检测系统中的多源数据融合与分析技术(171)
本文围绕 Java 大数据在智能安防入侵检测系统中的应用展开,剖析系统现状与挑战,阐释多源数据融合及分析技术,结合案例与代码给出实操方案,提升入侵检测效能。