这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 字节码是 Java 源码和 JVM 之间的中间表示。看懂字节码,能解释 i++ 与 ++i 的执行顺序、字符串拼接的真正实现、try-finally 的复制、switch 的跳转表、泛型擦除和异常表。
2.1 编译与查看
示例源码:
public class Calculator {
public static int add(int a, int b) {
int sum = a + b;
return sum;
}
}
编译:
javac Calculator.java
查看字节码:
javap -c Calculator
输出示例:
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
解释:
| 指令 | 含义 |
|---|---|
iload_0 |
加载第 1 个 int 参数 |
iload_1 |
加载第 2 个 int 参数 |
iadd |
int 相加 |
istore_2 |
存入第 3 个局部变量槽 |
iload_2 |
读取局部变量 |
ireturn |
返回 int |
2.2 class 文件结构
简化结构:
Magic Number
-> Version
-> Constant Pool
-> Access Flags
-> This Class / Super Class
-> Interfaces
-> Fields
-> Methods
-> Attributes
查看常量池:
javap -v Calculator
版本对应示例:
| major version | Java 版本 |
|---|---|
| 61 | Java 17 |
| 65 | Java 21 |
常见异常:
UnsupportedClassVersionError
表示运行时 JDK 版本低于 class 编译版本。生产要统一编译、构建和运行版本。
2.3 操作数栈
JVM 指令大多基于操作数栈执行。
iload_0
-> stack: a
iload_1
-> stack: a, b
iadd
-> stack: a + b
栈帧结构:
Stack Frame
|-- Local Variable Table
|-- Operand Stack
+-- Dynamic Linking / Return Address
示例:
int result = (a + b) * 2;
字节码流程:
iload_0
iload_1
iadd
iconst_2
imul
istore_3
2.4 i++ 与 ++i
public class Increment {
public static int post(int i) {
return i++;
}
public static int pre(int i) {
return ++i;
}
}
查看:
javap -c Increment
post 核心逻辑:
iload_0 读取 i
iinc 0, 1 局部变量 i 加 1
ireturn 返回旧值
pre 核心逻辑:
iinc 0, 1 局部变量 i 加 1
iload_0 读取新值
ireturn
这解释了为什么两者作为独立语句时效果相同,作为表达式时结果不同。
2.5 字符串拼接
Java 9 之后,javac 通常使用 StringConcatFactory 生成拼接调用,而不是直接生成 StringBuilder.append 链。
Java 8 常见形式:
String result = a + b;
等价逻辑:
String result = new StringBuilder()
.append(a)
.append(b)
.toString();
现代 JDK:
javap -c StringConcat
可能看到:
invokedynamic #x
// InvokeDynamic #0:makeConcatWithConstants
结论:
- 循环中显式使用
StringBuilder仍更清晰; - 不必假设编译器永远生成同一种拼接方式;
- 高频路径应做基准测试;
- 日志参数建议惰性求值。
2.6 条件与循环
public static String grade(int score) {
if (score >= 90) {
return "A";
} else if (score >= 60) {
return "B";
}
return "C";
}
控制指令:
| 指令 | 含义 |
|---|---|
if_icmpge |
int 比较 |
ifge |
与 0 比较 |
goto |
无条件跳转 |
tableswitch |
连续 case 跳转表 |
lookupswitch |
稀疏 case 查找表 |
循环:
for (int i = 0; i < 10; i++) {
sum += i;
}
字节码结构:
初始化
-> 条件检查
-> 循环体
-> 更新变量
-> goto 条件检查
2.7 方法调用指令
| 指令 | 场景 |
|---|---|
invokestatic |
静态方法 |
invokespecial |
构造器、private、super 调用 |
invokevirtual |
实例虚方法 |
invokeinterface |
接口方法 |
invokedynamic |
动态调用点,lambda、字符串拼接等 |
示例:
public class CallSite {
public static void main(String[] args) {
Runnable task = CallSite::hello;
task.run();
}
private static void hello() {
System.out.println("hello");
}
}
Lambda 通常通过 invokedynamic 延迟生成调用站点,具体实现与 JDK 版本有关。
2.8 异常表
public static int parse(String value) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return 0;
} finally {
System.out.println("done");
}
}
查看:
javap -c -v ExceptionDemo
异常表字段:
| 字段 | 含义 |
|---|---|
| from | try 起点 |
| to | try 终点 |
| target | handler 位置 |
| type | 异常类型 |
finally 通常会复制到多个退出路径。这也是复杂 finally 和 return 交互容易混淆的原因。
2.9 泛型与装箱
类型擦除:
List<String> names = new ArrayList<>();
names.add("Java");
String value = names.get(0);
字节码里常见的是:
List -> 原始类型
checkcast String -> 读取时转换
装箱:
Integer boxed = 10;
int primitive = boxed;
对应:
| 操作 | 方法 |
|---|---|
| int -> Integer | Integer.valueOf |
| Integer -> int | Integer.intValue |
高频循环中装箱拆箱会带来额外对象和 CPU 开销。
2.10 Record 与模式匹配
Java Record:
public record Point(int x, int y) {}
查看生成方法:
javap -p Point
常见生成内容:
- 全参构造器;
- 每个组件的访问器;
equals;hashCode;toString。
模式匹配编译后仍是类型检查和转换,具体优化随 JDK 版本变化。
2.11 ASM 与字节码增强
常见场景:
| 场景 | 工具 |
|---|---|
| AOP | ASM、ByteBuddy |
| Agent | java agent |
| Mock | Mockito |
| ORM 懒加载 | 动态代理 |
| 微服务链路 | 字节码增强 |
ByteBuddy 示例:
Class<?> type = new ByteBuddy()
.subclass(Object.class)
.method(ElementMatchers.isToString())
.intercept(FixedValue.value("dynamic"))
.make()
.load(getClass().getClassLoader(),
ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();
字节码增强要关注:
- 类加载器隔离;
- 性能开销;
- 安全风险;
- 与 JIT 优化交互;
- 出错时能否关闭。
本章小结
字节码把 Java 源码翻译成 JVM 可执行的栈式指令。通过 javap 可以看到局部变量表、操作数栈、常量池、方法调用和异常表。很多“语法糖”的行为只有在字节码层才清楚。生产诊断中,字节码用于验证编译结果、分析增强框架和理解底层执行成本。
思考题
i++和++i的字节码有什么差异?- 为什么循环中不建议反复创建拼接对象?
invokevirtual和invokeinterface有什么区别?- finally 在字节码中如何实现?
- 如何用
javap确认一段代码是否发生装箱?