《极客时间教程 - 深入拆解 Java 虚拟机》笔记
2024/8/7大约 12 分钟
《极客时间教程 - 深入拆解 Java 虚拟机》笔记
开篇词 为什么我们要学习 Java 虚拟机?

Java 代码是怎么运行的?
- 虚拟机视角:class 文件加载后存放到方法区(Method Area),运行时执行方法区内的代码
- 每次方法调用,虚拟机在当前线程的 Java 方法栈中生成栈帧,存放局部变量和操作数;方法退出时栈帧弹出舍弃

- 硬件视角:字节码无法直接执行,需翻译为机器码,HotSpot 有两种形式:
- 解释执行:逐条翻译字节码并执行
- 即时编译(JIT):将整个方法的字节码编译为机器码后执行

Java 的基本类型

Java 虚拟机是如何加载 Java 类的
- 加载 - 是指查找字节流,并且据此创建类的过程。
- 链接 - 是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。
- 验证 - 确保被加载类能够满足 Java 虚拟机的约束条件。
- 准备 - 为被加载类的静态字段分配内存。
- 解析 - 将符号引用解析为直接引用。
- 初始化 - 为标记为常量值的字段赋值,以及执行
<clinit>方法的过程。Java 虚拟机会通过加锁来确保类的<clinit>方法仅被执行一次。
JVM 是如何执行方法调用的?(上)
- 重载(Overload):方法名相同、参数类型不同,编译期即可识别
- 重写(Override):子类定义与父类方法名和参数类型相同的方法
重载方法识别的三阶段:
- 不考虑自动装拆箱和可变长参数
- 允许自动装拆箱,不允许可变长参数
- 同时允许自动装拆箱和可变长参数
JVM 是如何执行方法调用的?(下)
- 虚方法调用:
invokevirtual和invokeinterface指令- 目标方法为
final→ 静态绑定 - 否则 → 动态绑定(根据调用者的动态类型决定目标方法)
- 目标方法为
- 方法表:重写方法与父类被重写方法的索引值一致,运行时通过索引查找目标方法
- 内联缓存加速动态绑定:
- 单态内联缓存:记录调用者动态类型及目标方法,类型匹配则直接调用
- 类型不匹配 → 劣化为超多态内联缓存,之后直接使用方法表
JVM 是如何处理异常的?
- 每个方法对应一个异常表,异常时按异常表决定控制流转移
catch和finally代码块都会生成异常表条目

JVM 是如何实现反射的?
- 委派实现:默认委派给本地实现;调用超过 15 次后切换为动态实现(自动生成字节码,直接
invoke调用目标方法) - 性能开销来源:
- 变长参数导致的
Object数组 - 基本类型的自动装箱/拆箱
- 方法无法内联
- 变长参数导致的
JVM 是怎么实现 invokedynamic 的?(上)
- 方法句柄是
invokedynamic的基石:强类型、可直接执行的引用- 仅关心参数类型和返回类型,不关心所在类和方法名
- 权限检查在创建时完成,比反射调用更高效
- 调用方式:
invokeExact(严格匹配描述符)和invoke - 支持通过适配器方法句柄增删改参数
- 与反射一样是间接调用,面临无法内联的问题
JVM 是怎么实现 invokedynamic 的?(下)
invokedynamic指令抽象出调用点概念,首次执行时运行启动方法生成并绑定调用点,后续直接调用已绑定的方法- Lambda 表达式转函数式接口通过
invokedynamic实现,启动方法用 ASM 生成适配器类:- 未捕获变量:始终返回同一个适配器实例
- 捕获变量:每次新建适配器实例
- 两者性能上限均可达直接调用性能,捕获型借助逃逸分析避免实际新建实例
Java 对象的内存布局
压缩指针
- 每个 Java 对象有对象头(标记字段 + 类型指针)
- 标记字段:存储哈希码、GC 信息、锁信息等运行数据
- 类型指针:指向对象的类
- 64 位 VM 中对象头占 16 字节(标记字段 64 位 + 类型指针 64 位)
- 压缩指针:将 64 位指针压缩为 32 位,要求堆对象起始地址对齐至 8 的倍数,同时对类字段重排列以实现内存对齐
字段重排序
垃圾回收(上)
引用计数法
- 为每个对象维护引用计数器,计数为 0 时可回收
- 引用赋值时 +1,引用被重新赋值时 -1
- 致命缺陷:无法处理循环引用
可达性分析法
- 以 GC Roots 为起点,探索所有可达对象,未探索到的即为死亡
- GC Roots 包括:
- 栈帧中的局部变量
- 已加载类的静态变量
- JNI handles
- 已启动且未停止的 Java 线程
Stop-the-world
- 停止非 GC 线程工作,直到完成垃圾回收
- 造成 GC 暂停时间(GC pause)
垃圾回收方式
标记-复制
- 内存分为两等分,用
from和to指针维护,仅用from区域分配内存 - GC 时将存活对象复制到
to区域,交换from/to指针 - 优点:解决内存碎片化
- 缺点:堆空间利用率极低

标记-清除
- 将死亡对象内存标记为空闲,记录在空闲列表(free list)中
- 新建对象时从空闲列表分配内存

标记-整理
- 将存活对象聚集到内存起始位置,留下连续空闲空间
- 解决内存碎片化,但压缩算法带来性能开销

垃圾回收(下)
- 堆划分为新生代(Eden + 2 个 Survivor)和老年代
-XX:SurvivorRatio调整 Eden/Survivor 比例

Minor GC 流程:
- Eden + from Survivor 中的存活对象复制到 to Survivor
- 交换 from/to 指针
- 对象复制次数达 15 次(
-XX:+MaxTenuringThreshold)→ 晋升老年代 - 单个 Survivor 占用达 50%(
-XX:TargetSurvivorRatio)→ 较高复制次数的对象晋升老年代
新生代采用标记-复制算法,理想情况下 Eden 区对象基本死亡,复制数据少,效果极佳。
Java 内存模型
happens-before 规则:
- 锁:解锁 happens-before 对同一锁的加锁
- volatile:写 happens-before 对同一字段的读
- 线程启动:
Thread.start()happens-before 该线程第一个操作 - 线程终止:线程最后操作 happens-before 终止事件
- 线程中断:中断操作 happens-before 被中断线程收到中断事件
- 构造/析构:构造器最后操作 happens-before 析构器第一个操作
happens-before 具备传递性:X → Y,Y → Z,则 X → Z。
Java 虚拟机是怎么实现 synchronized 的?
- synchronized 代码块:字节码包含
monitorenter/monitorexit指令 - synchronized 方法:方法访问标记为
ACC_SYNCHRONIZED,进入时执行monitorenter,退出时(正常返回或抛异常)执行monitorexit
Java 语法糖与 Java 编译器
- 泛型擦除:Java 编译器将泛型替换为所能指代的最高层次类
即时编译(上)
- Java 8 起默认采用分层编译,分为 5 层:
- 0 层:解释执行
- 1 层:无 profiling 的 C1 代码
- 2 层:部分 profiling 的 C1 代码
- 3 层:全部 profiling 的 C1 代码
- 4 层:C2 代码
- 通常流程:解释执行 → 3 层 C1 编译 → 4 层 C2 编译
- 编译触发:由方法调用计数器和循环回边计数器触发,阈值根据待编译方法数动态调整
- OSR(On-Stack Replacement):在非方法入口处切换解释执行和编译代码,解决单次调用中包含热循环的优化问题
即时编译(下)
- profiling 收集:
- 解释执行:仅收集方法调用次数和循环回边次数
- 3 层 C1 代码:收集分支 profile(条件跳转)和类型 profile(类型相关指令)
- 基于 profile 的优化对程序执行做出假设,精简控制流和数据流
- 假设失败时触发去优化,退回解释执行并重新收集 profile
即时编译器的中间表达形式
- 即时编译器将 Java 字节码转换为 SSA IR,以便进行优化
Java 字节码(基础篇)
- 方法栈帧分为操作数栈和局部变量区,变量从局部变量区加载到操作数栈运算后存回
- 字节码类型:加载常量、操作数栈操作、局部变量访问、类相关、方法调用、数组操作、控制流、计算指令
方法内联(上)
- 方法内联:编译时将方法调用替换为目标方法体,消除调用开销
- 可在字节码或 IR 图层面进行,需映射参数和返回值
- 内联决策因素:强制规则、调用层数、路径热度、目标方法大小、IR 图大小
方法内联(下)
- 完全去虚化:通过类型推导或类层次分析,将虚方法调用转为直接调用,关键在于证明目标方法唯一
- 条件去虚化:增添类型比较,将虚方法调用转为类型测试 + 直接调用,借助类型 Profile 实现
HotSpot 虚拟机的 intrinsic
@HotSpotIntrinsicCandidate注解标记的方法,替换为基于特定 CPU 指令的高效实现- 实现方式:
- 桩程序:解释执行或 JIT 代码中使用
- 特殊 IR 节点:内联过程中替换,最终生成指定 CPU 指令
- 300+ 个 intrinsic,典型的:
Unsafe类:java.util.concurrent包间接使用String/Arrays类:生成高效的 SIMD 指令
逃逸分析
Iterable的 foreach 是语法糖,编译后转为Iterator的hasNext/next调用- 逃逸分析:确定指针动态范围的静态分析,判断新建对象是否逃逸
- 判断依据:对象是否存入堆中、是否作为方法调用的调用者/参数
- 基于逃逸分析的优化:
- 锁消除:消除不会逃逸对象的同步操作
- 标量替换:将对象拆散为单独字段,分布在栈或寄存器中
- 部分逃逸分析:附带控制流信息,将新建操作推延至逃逸分支
字段访问相关优化
- 字段值缓存:沿控制流缓存字段值,后续读取直接使用缓存(要求两次访问间无方法调用、内存屏障等)
- 冗余存储消除:两次字段存储间无读取,可消除第一次冗余存储
循环优化
- 循环无关代码外提:将值不变的表达式或检测外提至循环前,避免冗余计算(通过 Sea-of-Nodes IR + 节点调度 + 循环预测实现)
- 循环展开:重复多次迭代并减少循环次数,以空间换时间;特殊形式为完全展开
向量化
- 借助 CPU SIMD 指令(单指令多数据),实现指令级并行
- HotSpot 的向量化方式:通过 intrinsic 调用特定方法时替换为 SIMD 实现(点覆盖,仅对明确使用这些方法的应用有效)
注解处理器
基准测试框架 JMH(上)
- JMH:OpenJDK 开源项目,规避 JIT 等优化对性能测试的影响,降低操作系统和硬件干扰
- 用
@Benchmark注解标记测试方法,自动生成性能测试代码和配置
基准测试框架 JMH(下)
@Fork允许开发人员指定所要 Fork 出的 Java 虚拟机的数目。@BenchmarkMode允许指定性能数据的格式。@Warmup和@Measurement允许配置预热迭代或者测试迭代的数目,每个迭代的时间以及每个操作包含多少次对测试方法的调用。@State允许配置测试程序的状态。测试前对程序状态的初始化以及测试后对程序状态的恢复或者校验可分别通过@Setup和@TearDown来实现。
Java 虚拟机的监控及诊断工具(命令行篇)
jps:列出所有 Java 进程jstat:查看类加载、JIT、GC 信息,常用于检测 GC 问题和内存泄漏jmap:统计堆中对象,可导出二进制快照jinfo:打印/修改 JVM 配置参数jstack:输出线程栈轨迹、线程状态、锁信息,自动检测死锁jcmd:多功能工具,可实现除jstat外上述所有命令的功能
Java 虚拟机的监控及诊断工具(GUI 篇)
- Eclipse MAT:分析
jmap导出的堆快照- 直方图:各类实例数及 Shallow/Retained heap 总和
- 支配树:每个对象直接支配的对象
- JMC(Java Mission Control):性能监控工具,其 JFR(Java Flight Recorder)组件可以极低开销收集 JVM 性能数据
JNI 的运行机制
- native 方法链接方式:
- 按 JNI 规范命名 C 函数,虚拟机自动链接
- 在 C 代码中主动链接
- JNI 提供 API 让 C 代码使用 Java 特性,使用特殊数据结构和异常处理模式
- 引用类型:
- 局部引用:native 方法返回后失效,传入参数和大部分 API 返回值属于此类
- 全局引用:手动管理生命周期,两者均可阻止 GC 回收
JavaAgent 与字节码注入
Graal:用 Java 编译 Java
- 用 Java 实现的 JIT 编译器,通过 JVMCI 与 JVM 交互,将字节码转为机器码
- 性能:Java 程序略优于 C2;Scala 程序优势可达 10%~20%,得益于激进优化方式
Truffle:语言实现框架
- 语言实现框架,仅需实现词法/语法解析 + AST 解释器即可达到极佳性能
- 已实现:JavaScript、Ruby、R、Python、Sulong(支持 C/C++)
- 核心技术:
- Partial Evaluation:将目标程序 AST 作为编译时常量,特化解释器生成 Java 代码,再由 Graal JIT 编译为机器码
- 节点重写:收集 AST 节点类型 Profile 进行特化,类型不匹配时去优化并重新编译
- Polyglot:支持混用多种语言,不同 Truffle 语言可复用内存中同一对象
SubstrateVM:AOT 编译框架
- 定位:高启动性能、低内存开销、无缝衔接 C 代码的 Java 运行时
- 要求目标程序封闭(不能动态加载类库),AOT 编译时探索所有可能运行到的方法并全部编译
- 启动快、内存少:编译时已保存初始化好的堆快照,程序可直接从入口运行(对比 HotSpot 需大量初始化)
- Metropolis 项目:用 SubstrateVM 逐步替换 HotSpot 中的 C++ 代码,提升可维护性