字节码与执行引擎

核心概念

Java 程序编译后生成 Class 文件(字节码),由 JVM 的执行引擎解释或编译执行。理解字节码有助于深入理解 Java 语言特性的底层实现。

Class 文件结构

整体结构

ClassFile {
    u4             magic;                 // 魔数:0xCAFEBABE
    u2             minor_version;         // 次版本号
    u2             major_version;         // 主版本号(52=JDK 8, 61=JDK 17)
    u2             constant_pool_count;   // 常量池大小
    cp_info        constant_pool[];       // 常量池
    u2             access_flags;          // 访问标志
    u2             this_class;            // 类索引
    u2             super_class;           // 父类索引
    u2             interfaces_count;      // 接口数量
    u2             interfaces[];          // 接口索引
    u2             fields_count;          // 字段数量
    field_info     fields[];              // 字段表
    u2             methods_count;         // 方法数量
    method_info    methods[];             // 方法表
    u2             attributes_count;      // 属性数量
    attribute_info attributes[];          // 属性表
}

常量池

常量池是 Class 文件的资源仓库,包含两大类常量:

  • 字面量:文本字符串、final 常量值

  • 符号引用:类/接口全限定名、字段名和描述符、方法名和描述符

查看字节码

字节码指令

操作数栈与局部变量表

常用指令分类

加载和存储指令

算术指令

方法调用指令

实例分析

对应字节码:

i++ 与 ++i 的字节码区别

执行引擎

解释执行

  • 逐条读取字节码指令并执行

  • 启动速度快,执行速度慢

  • 所有 Java 代码都可以解释执行

JIT 编译(Just-In-Time)⭐⭐⭐⭐

将热点字节码编译为本地机器码,提升执行效率。

热点代码检测

  • 方法调用计数器:统计方法被调用的次数

  • 回边计数器:统计循环体执行的次数

  • 达到阈值后触发 JIT 编译

编译器类型

编译器
特点
编译速度
代码质量

C1(Client Compiler)

简单优化

一般

C2(Server Compiler)

深度优化

Graal

新一代编译器

分层编译(Tiered Compilation)

JDK 8+ 默认开启,结合 C1 和 C2:

JIT 优化技术

1. 方法内联(Inlining)

2. 逃逸分析(Escape Analysis)⭐⭐⭐⭐

分析对象的作用域,判断对象是否"逃逸"出方法或线程。

基于逃逸分析的优化:

  • 栈上分配:未逃逸的对象直接在栈上分配,方法结束自动回收

  • 标量替换:将对象拆解为基本类型变量

  • 锁消除:未逃逸的对象上的同步操作可以消除

方法调用

静态分派(编译期确定)

动态分派(运行期确定)

虚方法表(vtable)

  • 每个类有一个虚方法表

  • 重写的方法指向子类实现

  • 未重写的方法指向父类实现

  • 类加载的连接阶段初始化虚方法表

面试要点

高频问题

  1. JIT 编译器的作用?

    • 将热点代码编译为本地机器码,提升执行效率

  2. 什么是逃逸分析?有什么优化?

    • 分析对象是否逃逸出方法/线程

    • 栈上分配、标量替换、锁消除

  3. 方法调用的分派机制?

    • 静态分派:重载,编译期确定

    • 动态分派:重写,运行期确定(虚方法表)

  4. invokedynamic 指令的作用?

    • 支持动态类型语言

    • Lambda 表达式的底层实现

  5. i++ 是线程安全的吗?

    • 不是,i++ 包含读取、自增、写回三个步骤,不是原子操作

常见陷阱

  • 解释执行不等于慢,JIT 编译不等于快(编译本身有开销)

  • 逃逸分析不保证一定触发栈上分配(受 JVM 实现和参数影响)

  • final 方法不一定被内联,短方法更容易被内联

参考资料

Last updated