news 2026/5/15 3:09:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手把手看懂 Java 字节码:讲透 Integer 判等、静态方法重写与 try-finally 核心底层

前言

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

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、🌰🌰举个例子

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、字节码解析

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()方法,而它背后的对象缓存机制,才是导致结果差异的关键:

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

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

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方法的字节码

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、举个例子

package com.nl; public class ClassFlourDemo { public int execute() { try { return 1; } catch (Exception e) { return 2; } finally { end(); } } private void end(){ } }

3.2、字节码

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块的"复制"机制

invokespecial #2 <com/nl/ClassFlourDemo.end : ()V>
⚠️注意
✔️这条指令出现了 3次(第3行、第12行、第19行),说明编译器将 finally 块复制到了每个可能的退出路径上。

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

// 不是直接返回,而是: // 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底层知识。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/15 3:02:24

从技能树到实战:如何构建高效可实践的开发者学习框架

1. 项目概述与核心价值最近在GitHub上看到一个挺有意思的项目&#xff0c;叫“HappyHackingSpace/skills”。光看名字&#xff0c;你可能会觉得这又是一个普通的技能列表或者学习路线图仓库。但点进去之后&#xff0c;我发现它的定位和内容组织方式&#xff0c;和市面上那些“A…

作者头像 李华
网站建设 2026/5/15 2:54:57

系统架构设计师必知:数字签名、加密算法、公钥私钥详解

一、先搞清楚三个基础概念 在进入“数字签名”之前&#xff0c;必须先理解加密和哈希。 1.1 加密&#xff08;Encryption&#xff09; 加密是将明文通过某种算法转换成密文的过程&#xff0c;目的是保密。只有拥有正确密钥的人才能解密还原明文。对称加密&#xff1a;加密和解密…

作者头像 李华
网站建设 2026/5/15 2:48:20

pico示波器采集软件SSL1000A在功率器件测试的应用

在新能源汽车电控体系里&#xff0c;IGBT、MOSFET 是电机控制器、OBC、DC-DC 等核心模块的 “功率开关”&#xff0c;它们的开关特性、瞬态响应、稳定可靠性直接影响整车效率与安全。功率器件测试看似简单&#xff0c;实则细节要求极高&#xff0c;因为器件在高频开关中产生的尖…

作者头像 李华