JVMNotes

第 02 章:Java 字节码

zjc 于 2026-01-02 发布

这是《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

结论:

  1. 循环中显式使用 StringBuilder 仍更清晰;
  2. 不必假设编译器永远生成同一种拼接方式;
  3. 高频路径应做基准测试;
  4. 日志参数建议惰性求值。

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

常见生成内容:

  1. 全参构造器;
  2. 每个组件的访问器;
  3. equals
  4. hashCode
  5. 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();

字节码增强要关注:

  1. 类加载器隔离;
  2. 性能开销;
  3. 安全风险;
  4. 与 JIT 优化交互;
  5. 出错时能否关闭。

本章小结

字节码把 Java 源码翻译成 JVM 可执行的栈式指令。通过 javap 可以看到局部变量表、操作数栈、常量池、方法调用和异常表。很多“语法糖”的行为只有在字节码层才清楚。生产诊断中,字节码用于验证编译结果、分析增强框架和理解底层执行成本。

思考题

  1. i++++i 的字节码有什么差异?
  2. 为什么循环中不建议反复创建拼接对象?
  3. invokevirtualinvokeinterface 有什么区别?
  4. finally 在字节码中如何实现?
  5. 如何用 javap 确认一段代码是否发生装箱?