手把手看懂 Java 字节码:讲透 Integer 判等、静态方法重写与 try-finally 核心底层

前言

日常业务开发中,我遇到了一个很有意思的现象,先看这段极简代码:

ini 复制代码
Integer i1 = 128;
Integer i2 = 128;
System.out.println(i1 == i2);

运行输出结果为:false

初次看到这个结果时其实挺疑惑的,明明都是赋值 128,为什么用 == 判断却不相等?翻阅资料后才发现,这背后本质是 Integer 底层机制在起作用。

顺着这个问题往下深挖,就绕不开 Class 文件、字节码指令与包装类缓存的核心原理。下面我们就从根源入手,一步步拆解底层逻辑。


一、Class文件结构

JVM 只遵守一条规则:只认符合标准的 Class 文件 。只要你能生成符合该规范的 Class 文件,无论其来源如何,都能在任意兼容的 JVM 上运行。JVM 本身并不关心 Class 文件是由 Java、Kotlin 还是其他语言编译而来 ------ 这正是 JVM 能够跨语言支持的底层基础。

1、 🌰🌰举个例子

ini 复制代码
package com.nl;

public class ClassDemo {
    public static void main(String[] args) {
        Integer i3 = 128;
        Integer i4 = 128;
        System.out.println(i3 == i4);
    }
}

2、Class 文件

2.1、Class 文件本质上是一个纯二进制文件

它无法直接用普通文本编辑器阅读,但可以用十六进制工具(如 Sublime Text 等)打开查看

2.2、jclasslib 插件

不过,盯着这一串十六进制数字看,理解起来还是太抽象了。我们可以借助 IDEA 里的 jclasslib 插件,它能把 Class 文件的二进制内容,转换成清晰的结构化视图,效果大概是这样的:

2.3、LineNumberTable字节码对应代码表

⚠️ LineNumberTable 就是 字节码指令与 Java 源代码行号的映射表 ,它的作用是:

✔️让 JVM 在调试、异常堆栈时,能精准把字节码位置 对应到源代码的第几行,方便定位问题。

3、字节码解析

ruby 复制代码
0 sipush 128              // 将常量128压入操作数栈
3 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>         // 调用Integer.valueOf(128),返回Integer对象
6 astore_1                // 将返回的Integer对象存储到局部变量表索引1的位置(即变量a)
7 sipush 128              // 将常量128压入操作数栈
10 invokestatic #2 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>     // 调用Integer.valueOf(128),返回Integer对象
13 astore_2               // 将返回的Integer对象存储到局部变量表索引2的位置(即变量b)
14 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>           // 获取System.out静态字段(PrintStream对象)
17 aload_1                // 将局部变量a(Integer对象引用)压入栈
18 aload_2                // 将局部变量b(Integer对象引用)压入栈
19 if_acmpne 26           // 如果两个引用不相等,跳转到第26行
22 iconst_1               // 将常量1压入栈(表示true)
23 goto 27                // 跳转到第27行
26 iconst_0               // 将常量0压入栈(表示false)【跳转目标】
27 invokevirtual #4 <java/io/PrintStream.println : (Z)V>       // 调用println(boolean)方法输出结果
30 return                 // 方法返回

⚠️注意

✔️字节码第 0~7 行,对应源码中的 Integer i3 = 128

✔️ 从字节码中可以清晰看出:代码底层会自动调用 Integer.valueOf(128) 完成装箱赋值,最终返回 Integer 包装类对象。

⚠️是javap 反汇编后的字节码指令,工具把二进制解析后翻译成人读:

✔️把 0x11 这种机器码 → 翻译成 sipush

✔️把 0x36 → 翻译成 astore_1

✔️把索引 #2 → 翻译成 Integer.valueOf


二、面试题

1、明明两个数的值一模一样,为什么 == 比较后返回的是 false?

在上文的 《一、Class文件结构》 的例子, 查看字节码,发现这里的赋值本质上调用了 Integer.valueOf() 方法,而它背后的对象缓存机制,才是导致结果差异的关键:

arduino 复制代码
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
  • 当你写 Integer i = 128; 时,编译器会自动装箱成 Integer.valueOf(128)
  • Integer.valueOf() 会对 -128 ~ 127 之间的整数做缓存,超出这个范围会新建对象。
  • 所以 127 时两个对象是同一个,==true128 时是两个不同对象,==false

2、静态方法为什么不能被重写 Override

2.1、举个例子

ClassTwoDemo

csharp 复制代码
package com.nl;

public class ClassTwoDemo {

    public void execute() {
        test();
        ClassTwoDemo.testTwo();
    }

    public void test() {
        System.out.println("ClassTwoDemo.test()");
    }

    public static void testTwo() {
        System.out.println("ClassTwoDemo.testTwo()");
    }

}

ClassThreeDemo

scala 复制代码
package com.nl;

public class ClassThreeDemo extends ClassTwoDemo {

    @Override
    public void test() {
        System.out.println("ClassTwoDemo.testTwo()");
    }

    /**
     * 这个是报错的,静态方法不能被重写
     */
    @Override
    public void testTwo() {
        System.out.println("ClassTwoDemo.testTwo()");
    }
}

⚠️注意

✔️java: com.nl.ClassThreeDemo中的testTwo()无法覆盖com.nl.ClassTwoDemo中的testTwo()被覆盖的方法为static

2.2、为什么会报错呢?

execute方法的字节码

bash 复制代码
0 aload_0
1 invokevirtual #2 <com/nl/ClassTwoDemo.test : ()V>
4 invokestatic #3 <com/nl/ClassTwoDemo.testTwo : ()V>
7 return

⚠️注意

✔️从 JVM 字节码指令来看,invokevirtualinvokestatic 作用不同,静态方法和普通重载方法的调用指令并不一样

查阅相关资料可知,JVM 提供了多条方法调用字节码指令,各自分工不同

  • invokevirtual:调用对象的虚方法,也是日常中支持重写特性的实例方法;
  • invokespecial:依据编译时类型绑定调用实例方法,常用于构造方法 <init>、私有方法、父类方法调用,并不负责静态代码块;
  • invokestatic:专门调用类静态方法,不支持重写特性的实例方法
  • invokeinterface:用于调用接口中的抽象方法。

2.3、结论

静态方法底层固定使用 invokestatic 指令,仅做静态绑定、不具备动态绑定能力,故而语法上不允许被重写。

3、讲讲字节码try-cache-finally的执行流程?

3.1、举个例子

csharp 复制代码
package com.nl;

public class ClassFlourDemo {

    public int execute() {
        try {
            return 1;
        } catch (Exception e) {
            return 2;
        } finally {
            end();
        }
    }

    private void end(){

    }
}

3.2、字节码

arduino 复制代码
0 iconst_1          // 将常量1压入操作数栈
1 istore_1          // 存储到局部变量表索引1(暂存返回值)
2 aload_0           // 加载this引用(当前对象)
3 invokespecial #2 <com/nl/ClassFlourDemo.end : ()V>  // 调用 this.end() ← finally块执行
6 iload_1           // 加载暂存的返回值1
7 ireturn           // 返回1
8 astore_1          // 将异常引用存储到局部变量表索引1(即变量e)
9 iconst_2          // 将常量2压入操作数栈
10 istore_2         // 存储到局部变量表索引2(暂存返回值)
11 aload_0          // 加载this引用
12 invokespecial #2 <com/nl/ClassFlourDemo.end : ()V> // 调用 this.end() ← finally块执行
15 iload_2          // 加载暂存的返回值2
16 ireturn          // 返回2
17 astore_3         // 将异常引用存储到局部变量表索引3
18 aload_0          // 加载this引用
19 invokespecial #2 <com/nl/ClassFlourDemo.end : ()V> // 调用 this.end() ← finally块执行
22 aload_3          // 加载异常引用
23 athrow           // 重新抛出异常

3.3、finally块的"复制"机制

arduino 复制代码
invokespecial #2 <com/nl/ClassFlourDemo.end : ()V>

⚠️注意

✔️这条指令出现了 3次(第3行、第12行、第19行),说明编译器将 finally 块复制到了每个可能的退出路径上。

4、什么是返回值的暂存机制?

csharp 复制代码
// 不是直接返回,而是:
// 1. istore_2 暂存返回值(1)
// 2. 执行finally (x被改成999)
// 3. iload_2 加载暂存的返回值(1)
// 4. ireturn 真正返回(1)
    public int test() {
        int x = 1;
        try {
            return x;
        } finally {
            x = 999;  // 不会影响返回值
        }
    }

⚠️为什么需要暂存?

✔️因为 finally 可能在return之前修改数据

✔️Java保证:finally中的代码不会改变已经确定的返回值(对于基本类型)


三、总结

本文通过字节码,通俗易懂拆解了三个经典Java问题。

  • 借助jclasslib插件能直观看到,Integer自动装箱会调用valueOf方法,缓存机制导致判等结果出人意料;
  • 静态方法采用invokestatic静态绑定,没有动态绑定能力,所以不能重写;
  • try-finally语句,编译器会把finally代码复制到每一处退出路径,同时暂存返回值。

很多看似反常的代码现象,其实都能在字节码里找到答案。看懂字节码,不仅能轻松拿捏面试高频题,还能搞懂JVM底层运行逻辑,不用死记硬背,真正吃透Java底层知识。

相关推荐
踏浪无痕1 小时前
k8s发布服务,nacos未服务未下线紧急处理流程
后端
TYKJ0231 小时前
物理安全:顶级机房为什么需要刷脸+指纹+工牌
后端
程序员黑豆1 小时前
AI全栈开发 - Java:注释
前端·后端·ai编程
小二·1 小时前
Spring Boot 3 + Vue 3 全栈开发实战
vue.js·spring boot·后端
仿生joe会梦见漫天的大雪吗2 小时前
CTF学习笔记03:密码口令 —— 从弱口令到字典爆破
后端
自进化Agent智能体2 小时前
从零到一玩转Hermes Agent:VPS部署 × 模型配置 × 记忆架构 × 多Agent协作
后端
用户4682557459132 小时前
Testcontainers 在 Windows Docker Desktop 上跑不通:协议层不兼容 + 4 种可行环境
java·后端
Tenaryo2 小时前
「底层系统基石 · 缓存篇」V —— 写策略、Store Buffer 与内存屏障
后端·面试