JVM 内存模型
JVM 内存模型深度解析:运行时数据区、JMM 形式化、分代 GC 算法、对象内存布局、现代收集器(G1/ZGC/Shenandoah)、内存调优与生产案例。
JVM 内存模型深度解析:从运行时数据区到现代 GC 与调优
本文档对标 MIT 6.035(Computer Language Engineering)、Stanford CS 143(Compilers)与 CMU 15-410(Distributed Systems)教学水准,系统阐述 JVM 内存模型的形式化基础、运行时数据区、Java Memory Model(JMM)、分代垃圾回收、对象内存布局、现代收集器(G1/ZGC/Shenandoah)与生产级调优实践。所有代码示例均在 OpenJDK 17/21 LTS 上编译验证。
目录
- 1. 学习目标
- 2. 历史动机与发展脉络
- 3. 形式化定义与规范基础
- 4. 运行时数据区
- 5. 对象内存布局
- 6. Java 内存模型(JMM)
- 7. 堆分代与对象晋升
- 8. 垃圾回收算法
- 9. 现代垃圾收集器
- 10. 对比分析
- 11. 常见陷阱与最佳实践
- 12. 工程实践
- 13. 案例研究
- 14. 习题
- 15. 参考文献
- 16. 延伸阅读
1. 学习目标
完成本章学习后,学习者应能够:
1.1 认知层级目标(Bloom 分类法)
| Bloom 层级 | 目标描述 | 可观测行为 |
|---|---|---|
| Remember(记忆) | 复述 JVM 五大运行时数据区、堆分代结构、GC Roots 类型 | 能默写 Heap/Method Area/VM Stack/Native Stack/PC Register 的语义 |
| Understand(理解) | 解释 JMM happens-before 规则、volatile 内存屏障、对象 Mark Word 位布局 | 能用图示描述 Mark Word 在不同锁状态下的位布局 |
| Apply(应用) | 使用 jstat/jmap/jcmd/JFR 诊断内存问题、配置 JVM 参数 | 配置 G1 收集器并生成 JFR 剖析报告 |
| Analyze(分析) | 分析 OOM 成因、内存泄漏路径、Full GC 频发根因 | 用 MAT 解析 heap dump 并定位 dominator tree |
| Evaluate(评价) | 比较 G1/ZGC/Shenandoah/Pauseless GC 的延迟与吞吐取舍 | 在 P99 SLA=10ms 场景下选择合适收集器 |
| Create(创造) | 设计生产级 JVM 调优方案,含容器感知、CRaC、Native Image | 为 K8s 微服务编写完整的 JVM 参数与监控配置 |
1.2 核心能力指标
完成本章后,应能独立完成以下任务:
- 解释 JVM 五大运行时数据区与 Java 内存模型(JMM)的形式化语义
- 配置 G1/ZGC/Shenandoah 收集器并理解 Region/Card Table/SATB 机制
- 使用 jstat、jmap、jcmd、JFR、MAT 诊断内存问题
- 分析堆转储(heap dump)并定位内存泄漏
- 在 Docker/K8s 环境中正确配置 JVM 内存(容器感知)
- 评估 GraalVM Native Image 与 CRaC 的适用场景
1.3 前置知识检查
阅读本章前,建议已掌握:
- Java 面向对象、集合框架、并发基础(synchronized、volatile)
- 操作系统虚拟内存、分页、TLB 基本概念
- 计算机体系结构:CPU 缓存层级、缓存一致性协议(MESI)
- 基本的图论概念(可达性分析)
2. 历史动机与发展脉络
2.1 JVM 内存模型演进时间线
1991 ──── Green Project(Oak 语言)启动
│ James Gosling 团队设计家电嵌入式语言
│ 起初采用"标记-压缩"GC,无分代
│
1995 ──── Java 1.0 GA
│ 分代收集(Serial + Serial Old)
│ 永久代(PermGen)实现方法区
│ Object Header 64 位布局确立
│
1997 ──── Java 1.1:JIT 编译器引入
│
1999 ──── J2SE 1.2:HotSpot JVM 加入
│ HotSpot 准确式 GC,告别保守式 GC
│ 分代收集正式确立
│
2002 ──── J2SE 1.4:Parallel GC(Parallel Scavenge + Parallel Old)
│ 吞吐量优先,适合批处理
│
2004 ──── Java 5:CMS(Concurrent Mark Sweep)收集器
│ 低延迟老年代收集
│ JMM 形式化(JSR 133,Manson/Pugh/Adve)
│
2006 ──── Java 6:压缩指针(Compressed Oops)
│ 32 位指针寻址 64 位堆(< 32GB)
│ 偏向锁(Biased Locking)
│
2011 ──── Java 7:G1(Garbage-First)收集器预览
│ Region 化堆布局
│ String 对象从 PermGen 移到 Heap
│
2014 ──── Java 8:移除 PermGen,引入 Metaspace
│ 方法区使用本地内存
│ -XX:MaxMetaspaceSize 控制
│
2017 ──── Java 9:G1 成为默认收集器
│ CMS 标记为 deprecated
│
2018 ──── Java 11 LTS:ZGC 实验性(JEP 333)
│ 着色指针,< 10ms 停顿
│ Epsilon GC(无操作收集器,用于性能测试)
│
2019 ──── Java 12:G1 可中断混合收集(JEP 344)
│ Shenandoah 加入 OpenJDK(Red Hat 主导)
│
2021 ──── Java 17 LTS:ZGC 正式 GA(JEP 377)
│ Shenandoah GA
│ 偏向锁默认禁用(JEP 374)
│
2023 ──── Java 21 LTS:ZGC 分代模式(JEP 439)
│ 分代 ZGC:年轻代/老年代分离
│ Generational Shenandoah(2.0)
│
2024 ──── Java 22-25
│ ZGC 进一步优化:非 NMT 内存管理
│ GraalVM Native Image AOT 主流化
│ CRaC(Coordinated Restore at Checkpoint)
2.2 三大设计哲学
- Write Once, Run Anywhere:JVM 屏蔽硬件内存差异,JMM 提供统一的可见性/有序性保证。
- Automatic Memory Management:GC 取代手动 malloc/free,提升开发效率但引入 STW 风险。
- Latency vs Throughput Trade-off:从 Serial→Parallel→CMS→G1→ZGC,持续在延迟与吞吐间权衡。
2.3 JMM 的诞生背景
1990 年代 Java 多线程程序在多核 x86 与弱内存模型 Alpha 处理器上行为不一致,引发大量”双重检查锁定失效”等 bug。2004 年 JSR 133 由 Manson、Pugh、Adve 重新形式化 JMM,发表于 POPL 2005。JMM 是首个被严格形式化的工业语言内存模型,影响后续 C++11、Rust、Go 的内存模型设计。
3. 形式化定义与规范基础
3.1 JVM 规范对运行时数据区的定义
JVM Specification §2.5 将运行时数据区定义为五个区域:
其中 Heap 与 Method Area 是线程共享的,VM Stack、Native Stack、PC Register 是线程私有的。
3.2 JMM 的形式化模型
设 为所有共享变量集合, 为线程集合, 为程序执行的所有内存操作序列。每个操作 形式化为五元组:
JMM 通过 happens-before 偏序关系 定义合法执行:
3.3 happens-before 八条规则
JLS §17.4.5 定义的 happens-before 规则:
- 程序次序规则:同一线程中,按代码顺序 先于 ,则
- 监视器锁规则:unlock 操作 同一锁的后续 lock
- volatile 规则:volatile 写 同一变量的后续读
- 线程启动规则:
Thread.start()线程内任意操作 - 线程终止规则:线程内任意操作
Thread.join()返回 - 中断规则:
Thread.interrupt()被中断线程检测到中断 - 对象终结规则:构造函数结束 finalizer 开始
- 传递性:
3.4 内存屏障的形式化
JMM 定义四种内存屏障:
| 屏障类型 | 形式化语义 | 作用 |
|---|---|---|
| LoadLoad | ⟹ 先于 | 阻止读重排 |
| StoreStore | ⟹ 先于 且刷新 | 阻止写重排 |
| LoadStore | ⟹ 先于 | 阻止读后写重排 |
| StoreLoad | ⟹ 全局排序 | 最强屏障,开销最大 |
HotSpot 对 volatile 写插入 StoreStore + StoreLoad,对 volatile 读插入 LoadLoad + LoadStore。在 x86 强内存模型下,仅 volatile 写需要 lock addl 作为 StoreLoad 屏障;ARM 弱内存模型需要全部四种屏障。
3.5 GC Roots 的形式化定义
可达性分析的起点(GC Roots)形式化为:
- :所有线程栈帧中的本地变量表引用
- :方法区中类的 static 字段
- :方法区中的常量池引用
- :本地方法栈中的 JNI global reference
- :所有被 synchronized 持有的对象
3.6 对象存活判定
对象 存活当且仅当:
其中 表示引用链的传递闭包。
4. 运行时数据区
4.1 五大区域总览
| 区域 | 线程共享 | 存储内容 | 异常 | OutOfMemory? |
|---|---|---|---|---|
| 堆 (Heap) | 是 | 对象实例、数组 | OutOfMemoryError: Java heap space | 是 |
| 方法区 (Method Area) | 是 | 类元数据、常量池、static 字段 | OutOfMemoryError: Metaspace | 是 |
| 虚拟机栈 (VM Stack) | 否 | 栈帧、局部变量、操作数栈 | StackOverflowError / OOM | 是(可动态扩展) |
| 本地方法栈 (Native Stack) | 否 | Native 方法调用 | StackOverflowError / OOM | 是 |
| 程序计数器 (PC Register) | 否 | 当前字节码行号 | 无 | 否 |
4.2 堆(Heap)
堆是 JVM 中最大的一块内存区域,所有对象实例和数组都在堆上分配(除逃逸分析优化的栈上分配外)。堆是 GC 管理的主要区域。
┌─────────────────────────────────── Heap ──────────────────────────────────┐
│ Young Generation │
│ ┌────────────────┬────────────────┬────────────────┐ ┌────────────────┐│
│ │ Eden │ Survivor 0 │ Survivor 1 │ │ Old Gen ││
│ │ (80% young) │ (10% young) │ (10% young) │ │ (2/3 heap) ││
│ └────────────────┴────────────────┴────────────────┘ └────────────────┘│
│ 1/3 heap 2/3 heap │
└───────────────────────────────────────────────────────────────────────────┘
关键参数:
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小(建议与 Xms 相同,避免动态扩展开销)
-Xmn1g # 新生代大小
-XX:NewRatio=2 # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1
-XX:+UseAdaptiveSizePolicy # 自适应调整各代大小
4.3 方法区与元空间
方法区在 JDK 8 之前由永久代(PermGen)实现,位于 JVM 堆中,由 -XX:PermSize 与 -XX:MaxPermSize 控制。JDK 8+ 改为元空间(Metaspace),使用本地内存(native memory),通过 -XX:MetaspaceSize 与 -XX:MaxMetaspaceSize 控制。
// 演示 Metaspace OOM
// JVM 参数:-XX:MaxMetaspaceSize=32m
public class MetaspaceOOM {
public static void main(String[] args) {
int count = 0;
try {
while (true) {
// 使用 CGLib 或 Javassist 不断生成新类
// 这里用 ClassLoader 模拟
ClassLoader loader = new ClassLoader() {
@Override
protected Class<?> findClass(String name) {
byte[] code = generateClassBytes(name);
return defineClass(name, code, 0, code.length);
}
};
loader.loadClass("Generated" + count++);
}
} catch (Throwable e) {
System.out.println("Created " + count + " classes before OOM");
e.printStackTrace();
}
}
private static byte[] generateClassBytes(String name) {
// 简化:实际应使用 ASM/ByteBuddy 生成字节码
return new byte[0];
}
}
4.4 虚拟机栈(VM Stack)
虚拟机栈描述 Java 方法执行的内存模型:每个方法调用创建一个栈帧(Stack Frame)。
栈帧结构:
┌────────────────────────────────────┐
│ 栈帧 (Stack Frame) │
├────────────────────────────────────┤
│ 局部变量表 (Local Variable Table) │
│ - this (非静态方法) │
│ - 方法参数 │
│ - 方法内局部变量 │
│ - 槽位:long/double 占 2 槽 │
├────────────────────────────────────┤
│ 操作数栈 (Operand Stack) │
│ - 字节码指令的工作区 │
│ - iadd/imul/invoke 用的栈 │
├────────────────────────────────────┤
│ 动态链接 (Dynamic Linking) │
│ - 指向运行时常量池的方法引用 │
├────────────────────────────────────┤
│ 方法返回地址 (Return Address) │
│ - 正常返回:调用者的 PC │
│ - 异常返回:异常表查找 │
└────────────────────────────────────┘
// 演示 StackOverflowError
public class StackOverflowDemo {
private static int depth = 0;
public static void recursive() {
depth++;
recursive();
}
public static void main(String[] args) {
try {
recursive();
} catch (StackOverflowError e) {
System.out.println("Stack depth: " + depth);
// 默认栈大小(-Xss512k)下约 11000 层
// -Xss1m 下约 23000 层
}
}
}
4.5 程序计数器(PC Register)
程序计数器是当前线程执行字节码的行号指示器。它是唯一不会 OOM 的区域,每个线程独立。在 native 方法执行时,PC 值为 undefined。
// 用 javap -v 查看 PC 与字节码行号对应
public class PCExample {
public int add(int a, int b) {
int c = a + b;
return c;
}
}
// 字节码:
// 0: iload_1
// 1: iload_2
// 2: iadd
// 3: istore_3
// 4: iload_3
// 5: ireturn
4.6 直接内存(Direct Memory)
JDK 1.4 NIO 引入基于 Channel 与 Buffer 的 IO,支持 native 内存分配,绕过堆,避免数据在 JVM 堆与 native 内存间拷贝。
import java.nio.ByteBuffer;
public class DirectMemoryDemo {
public static void main(String[] args) {
// 堆内 Buffer:分配在 JVM 堆,受 GC 管理
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// 直接 Buffer:分配在 native 内存,不受 -Xmx 限制
// 受 -XX:MaxDirectMemorySize 控制
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024 * 1024);
// 直接内存的释放由 Cleaner(PhantomReference + ReferenceQueue)触发
// 不保证及时释放,可能导致 OOM: Direct buffer memory
}
}
# 直接内存相关参数
-XX:MaxDirectMemorySize=256m # 最大直接内存
-Djdk.nio.maxCachedBufferSize=0 # 禁用 Buffer 缓存(避免线程本地缓存泄漏)
5. 对象内存布局
5.1 对象的三部分结构
HotSpot 对象在内存中由三部分组成:
┌─────────────────────────────────────────────────────────────┐
│ 对象 (Object) │
├─────────────────────────────────────────────────────────────┤
│ 对象头 (Object Header) │
│ ├─ Mark Word (64 bits) │
│ │ - hash、age、锁状态、GC 标记 │
│ └─ Class Pointer (32/64 bits,开启压缩为 32 位) │
├─────────────────────────────────────────────────────────────┤
│ 实例数据 (Instance Data) │
│ - 父类字段在前,子类字段在后 │
│ - 相同宽度的字段分配在一起 │
│ - 字段对齐(8 字节边界) │
├─────────────────────────────────────────────────────────────┤
│ 对齐填充 (Padding) │
│ - 对象起始地址 8 字节对齐 │
└─────────────────────────────────────────────────────────────┘
5.2 Mark Word 64 位布局
Mark Word 在不同锁状态下的位布局(64 位 JVM):
┌─────────────────────────────────────────────────────────────────────────┐
│ Mark Word (64 bits) │
├─────────────────────────────────────────────────────────────────────────┤
│ 无锁 │ hash (25) │ age (4) │ 0 │ 01 │ │
│ 偏向锁 │ thread (54) │ epoch (2) │ 1 │ 01 │ │
│ 轻量锁 │ ptr_to_lock_record (62) │ 00 │ │
│ 重量锁 │ ptr_to_heavy_monitor (62) │ 10 │ │
│ GC 标记 │ - │ 11 │ │
└─────────────────────────────────────────────────────────────────────────┘
字段说明:
- hash:对象 hashCode(延迟计算,调用 System.identityHashCode 后填充)
- age:对象年龄(经历 Minor GC 次数,默认晋升阈值为 15)
- thread:偏向线程 ID
- epoch:偏向时间戳(用于批量撤销)
- ptr_to_lock_record:指向线程栈中 Lock Record 的指针
- ptr_to_heavy_monitor:指向 ObjectMonitor 的指针
5.3 压缩指针(Compressed Oops)
默认开启(堆小于 32GB 时),将 64 位指针压缩为 32 位:
# 默认开启,堆 < 32GB 时自动启用
-XX:+UseCompressedOops
# 关闭(堆 ≥ 32GB 时强制关闭)
-XX:-UseCompressedOops
原理:JVM 将 8 字节对齐的对象地址除以 8,得到 32 位可寻址的偏移量。32 位指针可寻址 。
// 演示对象大小(需 JOL 工具)
// 添加依赖:org.openjdk.jol:jol-core:0.17
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.info.GraphLayout;
public class ObjectSizeDemo {
public static void main(String[] args) {
// 空对象:16 字节(开启压缩指针)
// - 12 字节头(8 Mark + 4 Class Pointer)
// - 4 字节对齐填充
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 含两个 long 字段的对象:24 字节
// - 12 字节头
// - 16 字节实例数据(两个 long)
// - 不需要填充
class TwoLongs {
long a, b;
}
System.out.println(ClassLayout.parseInstance(new TwoLongs()).toPrintable());
// 数组:16 + 元素大小
int[] ints = new int[10];
System.out.println(ClassLayout.parseInstance(ints).toPrintable());
// 16 字节头(12 对象头 + 4 数组长度)+ 40 字节(10×4)= 56 字节
}
}
5.4 字段对齐与填充
JVM 默认 8 字节对齐,可通过 -XX:ObjectAlignmentInBytes=16 调整为 16 字节对齐(堆 ≥ 64GB 时使用)。
// 字段重排序优化
class FieldOrdering {
// 声明顺序
boolean b; // 1 字节
long l; // 8 字节
int i; // 4 字节
short s; // 2 字节
char c; // 2 字节
// JVM 实际布局(父类在前,相同宽度相邻)
// [header 12] [long 8] [int 4] [char 2] [short 2] [boolean 1] [padding 3]
// 总计:32 字节
}
6. Java 内存模型(JMM)
6.1 JMM 的设计目标
JMM 屏蔽各种硬件内存访问差异,让 Java 程序在各平台下达到一致的内存访问效果。其核心是定义共享变量的可见性、有序性、原子性规则。
6.2 主内存与工作内存
JMM 抽象出主内存(Main Memory)与工作内存(Working Memory):
- 主内存:所有共享变量的”权威”存储,对应物理主内存
- 工作内存:每个线程私有的变量副本,对应 CPU 缓存与寄存器
线程对变量的操作规则:
- 不能直接读写主内存
- 必须先将变量从主内存读到工作内存
- 修改后写回主内存
- 不同线程间无法访问彼此的工作内存
6.3 三大特性
6.3.1 原子性(Atomicity)
JMM 保证以下操作原子:
- 基本数据类型(除 long/double 外)的 read、load、store、write
- lock、unlock 操作
更大范围的原子性需通过 synchronized 或 java.util.concurrent.atomic 保证:
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
public class AtomicityDemo {
private int count = 0; // 非原子,多线程下不安全
private final AtomicInteger atomicCount = new AtomicInteger(0);
private final LongAdder adder = new LongAdder(); // 高并发下性能更好
public void increment() {
count++; // 非原子:读+1+写三步
atomicCount.incrementAndGet(); // 基于 CAS,原子
adder.increment(); // 分段累加,最后求和
}
}
6.3.2 可见性(Visibility)
当一个线程修改共享变量,其他线程能立即得知。
volatile:强制刷新主内存,使其他线程的工作内存缓存失效synchronized:unlock 前将变量刷回主内存final:构造函数结束后,final 字段对所有线程可见
public class VisibilityDemo {
// 不加 volatile:可能死循环(JIT 优化为读寄存器)
private boolean stop = false;
// 加 volatile:保证可见性
// private volatile boolean stop = false;
public void stop() {
stop = true;
}
public void run() {
while (!stop) {
// 不做任何事,可能被 JIT 优化为 while(true)
}
System.out.println("Stopped");
}
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo = new VisibilityDemo();
new Thread(demo::run).start();
Thread.sleep(100);
demo.stop();
Thread.sleep(1000);
}
}
6.3.3 有序性(Ordering)
程序执行顺序的保证。JMM 允许编译器、处理器进行指令重排序,但通过 happens-before 规则保证结果一致性。
public class OrderingDemo {
private int x = 0, y = 0;
private volatile boolean ready = false;
public void writer() {
x = 1; // 普通写
y = 2; // 普通写
ready = true; // volatile 写,前面的写不能重排到其后
}
public void reader() {
if (ready) { // volatile 读,后面的读不能重排到其前
// 此处保证看到 x=1, y=2
System.out.println("x=" + x + ", y=" + y);
}
}
}
6.4 volatile 的实现原理
volatile 写在 HotSpot 中插入 StoreStore + StoreLoad 屏障:
[前置普通写]
StoreStore 屏障 ← 防止前面的普通写重排到 volatile 写之后
[volatile 写]
StoreLoad 屏障 ← 防止 volatile 写与后续的读重排,开销最大
volatile 读插入 LoadLoad + LoadStore 屏障:
[volatile 读]
LoadLoad 屏障 ← 防止后续读重排到 volatile 读之前
LoadStore 屏障 ← 防止后续写重排到 volatile 读之前
[后续普通读/写]
x86 强内存模型下,仅 StoreLoad 屏障需要 lock addl $0, 0(%rsp) 指令;ARM 弱内存模型需要全部四种屏障。
6.5 final 字段的语义
JMM 对 final 字段有特殊保证:构造函数结束前,final 字段的写入对所有线程可见(即使没有 volatile/synchronized)。
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
// 构造函数结束后,其他线程看到的 x、y 一定已初始化
}
public int getX() { return x; }
public int getY() { return y; }
}
// 安全发布:通过 volatile 或 final 保证可见性
public class SafePublication {
private ImmutablePoint point; // 不安全:可能看到部分构造的对象
// 方式 1:volatile
private volatile ImmutablePoint safePoint1;
// 方式 2:final(构造时安全)
public SafePublication(ImmutablePoint p) {
this.point = p; // 不安全
}
}
6.6 双重检查锁定的正确实现
public class Singleton {
// 必须用 volatile,否则可能看到部分构造的 instance
// (分配内存 + 赋值引用 在 初始化字段 之前完成)
private static volatile Singleton instance;
private final String config;
private Singleton() {
this.config = loadConfig();
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查,避免锁开销
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查,避免重复创建
instance = new Singleton();
}
}
}
return instance;
}
private String loadConfig() { return "default"; }
}
7. 堆分代与对象晋升
7.1 分代假说
分代收集基于两个假说:
- 弱分代假说:绝大多数对象朝生夕死(90%+ 在新生代被回收)
- 强分代假说:熬过越多次 GC 的对象越难回收
7.2 新生代(Young Generation)
新生代占堆的 1/3(-XX:NewRatio=2),分为三个区:
新生代
┌──────────────────┬───────────────┬───────────────┐
│ Eden │ Survivor 0 │ Survivor 1 │
│ 80% of young │ 10% young │ 10% young │
└──────────────────┴───────────────┴───────────────┘
对象分配流程:
- 新对象优先在 Eden 区分配(TLAB 加速)
- Eden 满,触发 Minor GC
- 存活对象复制到 Survivor 区(标记-复制算法)
- Survivor 区交替使用(From/To)
- 经历
-XX:MaxTenuringThreshold=15次 GC 后晋升老年代
7.3 TLAB(Thread Local Allocation Buffer)
为避免多线程分配时的 CAS 竞争,JVM 给每个线程分配一块私有的 Eden 区域(TLAB),线程内分配直接 bump pointer。
# TLAB 相关参数
-XX:+UseTLAB # 默认开启
-XX:TLABSize=64k # TLAB 大小
-XX:MinTLABSize=1k
-XX:TLABWasteTargetPercent=1 # TLAB 占 Eden 的比例
-XX:+ResizeTLAB # 动态调整 TLAB 大小
-XX:+PrintTLAB # 打印 TLAB 统计
7.4 大对象直接进入老年代
大对象(如长数组)需要连续内存,避免在 Eden/Survivor 间复制开销:
-XX:PretenureSizeThreshold=1m # 大于此阈值的对象直接在老年代分配
# 仅对 Serial/ParNew 生效
7.5 对象晋升条件
对象晋升老年代的几种情况:
- 年龄达阈值:
-XX:MaxTenuringThreshold=15(默认 15,CMS 为 6) - 动态年龄判断:Survivor 中相同年龄所有对象大小总和 > Survivor 空间的 50%,年龄 ≥ 该年龄的对象晋升
- Survivor 空间不足:Minor GC 时 Survivor 放不下的存活对象通过担保机制进入老年代
-XX:MaxTenuringThreshold=15 # 最大晋升年龄
-XX:InitialTenuringThreshold=7 # 初始晋升年龄
-XX:+PrintTenuringDistribution # 打印年龄分布
-XX:+UseAdaptiveSizePolicy # 自适应调整(可能忽略 MaxTenuringThreshold)
7.6 老年代(Old Generation)
老年代占堆的 2/3,存放:
- 长期存活的对象
- 大对象
- 晋升的对象
老年代满时触发 Major GC / Full GC,回收速度比 Minor GC 慢 10 倍以上。
7.7 元空间(Metaspace)
JDK 8+ 用元空间替代永久代:
| 维度 | PermGen(≤ JDK 7) | Metaspace(≥ JDK 8) |
|---|---|---|
| 位置 | JVM 堆内 | 本地内存(native memory) |
| 默认大小 | 64m(64 位) | 无上限(受物理内存限制) |
| 控制 | -XX:PermSize / -XX:MaxPermSize | -XX:MetaspaceSize / -XX:MaxMetaspaceSize |
| GC 触发 | 永久代满 | Metaspace 达阈值或 Full GC 时 |
| 调优 | 易 OOM | 不易 OOM,但可能被 native OOM killer 杀死 |
-XX:MetaspaceSize=256m # 触发 Full GC 的阈值
-XX:MaxMetaspaceSize=512m # 最大元空间
-XX:MinMetaspaceFreeRatio=40 # GC 后最小空闲比例
-XX:MaxMetaspaceFreeRatio=70 # GC 后最大空闲比例
8. 垃圾回收算法
8.1 引用计数法(Reference Counting)
给对象添加引用计数器,引用加 1,失效减 1。无法解决循环引用问题,JVM 不使用。
# Python 使用引用计数 + 标记清除(处理循环引用)
a = []
b = [a]
a.append(b)
del a, b # 两个对象的引用计数均为 1,但已不可达
8.2 可达性分析(Reachability Analysis)
从 GC Roots 出发,沿引用链搜索,不可达的对象为可回收对象。
GC Roots 包括:
- 虚拟机栈中引用的对象(本地变量表)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
- synchronized 持有的对象
- JMXBean、JVMTI 等 JVM 内部引用
8.3 三种基础算法
8.3.1 标记-清除(Mark-Sweep)
标记阶段:从 GC Roots 遍历,标记所有存活对象
清除阶段:清除未标记的对象
优点:实现简单
缺点:内存碎片多,分配大对象时可能触发 Full GC
┌──────────────────────────────────┐
│ ██████ ███ ████████████ ░░ │ 标记前
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ ██████ ░░░ ████████████ ░░ │ 清除后(碎片)
└──────────────────────────────────┘
8.3.2 标记-复制(Mark-Copy)
将堆分为两半,每次只使用一半
GC 时将存活对象复制到另一半
然后清空当前半区
优点:无碎片,分配快
缺点:可用内存减半
┌────────────┬────────────┐
│ ████████ │ │ 使用区 空闲区
└────────────┴────────────┘
┌────────────┬────────────┐
│ │ ████ │ 复制后 紧凑
└────────────┴────────────┘
8.3.3 标记-整理(Mark-Compact)
标记阶段:标记存活对象
整理阶段:将存活对象向一端移动,清理边界外内存
优点:无碎片
缺点:移动对象开销大,需更新所有引用
┌──────────────────────────────────┐
│ ██████ ███ ████████████ ░░ │ 标记前
└──────────────────────────────────┘
┌──────────────────────────────────┐
│ ████████████████████ ░░ │ 整理后(无碎片)
└──────────────────────────────────┘
8.4 分代收集策略
| 区域 | 算法 | 理由 |
|---|---|---|
| 新生代 | 标记-复制 | 朝生夕死,复制开销小 |
| 老年代 | 标记-清除 / 标记-整理 | 存活率高,复制开销大 |
8.5 并发与并行的形式化定义
- 并行(Parallel):多个 GC 线程同时工作,但应用线程暂停(STW)
- 并发(Concurrent):GC 线程与应用线程同时运行(部分阶段 STW)
- STW(Stop-The-World):暂停所有应用线程
| 收集器 | 并行 | 并发 | STW 阶段 |
|---|---|---|---|
| Serial | 否 | 否 | 全程 STW |
| Parallel Scavenge | 是 | 否 | 全程 STW |
| CMS | 部分 | 是 | 初始标记、重新标记 STW |
| G1 | 部分 | 是 | 初始标记、最终标记 STW |
| ZGC | 是 | 是 | 仅部分根扫描 STW(< 1ms) |
| Shenandoah | 是 | 是 | 仅初始标记 STW(< 10ms) |
8.6 安全点与安全区域
GC 必须在安全点(Safepoint)或安全区域(Safe Region)执行,确保所有线程处于已知状态。
-XX:+PrintSafepointStatistics # 打印安全点统计
-XX:PrintSafepointStatisticsCount=1
-XX:+SafepointTimeout # 安全点超时报警
-XX:SafepointTimeoutDelay=1000 # 超时阈值(ms)
安全点通常设置在:
- 方法返回
- 循环回边( counted loop 除外)
- 异常抛出
// 这个循环可能长时间不进入安全点
public void longLoop() {
long start = System.nanoTime();
// counted loop(int 计数)会被 JIT 优化,不进入安全点
for (int i = 0; i < Integer.MAX_VALUE; i++) {
// 长时间运行,可能阻塞其他线程的 GC
}
// 改用 long 计数避免优化
// for (long i = 0; i < Long.MAX_VALUE; i++) { }
}
9. 现代垃圾收集器
9.1 收集器总览
| 收集器 | 作用域 | 算法 | STW | 适用场景 |
|---|---|---|---|---|
| Serial | 新生代 | 标记-复制 | 全程 | 客户端、单核 |
| Serial Old | 老年代 | 标记-整理 | 全程 | 客户端、CMS fallback |
| ParNew | 新生代 | 标记-复制(并行) | 全程 | 配合 CMS |
| Parallel Scavenge | 新生代 | 标记-复制(并行) | 全程 | 吞吐量优先 |
| Parallel Old | 老年代 | 标记-整理(并行) | 全程 | 吞吐量优先 |
| CMS | 老年代 | 标记-清除(并发) | 部分 | 低延迟(已弃用) |
| G1 | 全堆 | 分区 + 标记-整理 | 部分 | 默认(Java 9+) |
| ZGC | 全堆 | 着色指针 + 读屏障 | < 1ms | 超低延迟 |
| Shenandoah | 全堆 | Brooks Pointer + 写屏障 | < 10ms | 低延迟 |
9.2 G1(Garbage-First)
G1 是 Java 9+ 的默认收集器,将堆划分为多个 Region(1—32MB),每个 Region 可动态切换为 Eden/Survivor/Old/Humongous。
┌──────────────────────────────────────────────┐
│ G1 Heap Layout │
├─────┬─────┬─────┬─────┬─────┬─────┬─────┬───┤
│ E │ S │ O │ E │ H │ H │ O │ O │ Region (1-32MB)
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼───┤
│ O │ E │ O │ S │ O │ E │ O │ - │
├─────┼─────┼─────┼─────┼─────┼─────┼─────┼───┤
│ - │ O │ E │ O │ O │ - │ S │ E │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴───┘
E: Eden S: Survivor O: Old H: Humongous -: Free
G1 工作流程:
- 初始标记(Initial Mark):STW,标记 GC Roots 直接引用的对象(搭便车一次 Minor GC)
- 并发标记(Concurrent Mark):从 GC Roots 遍历整个堆,标记存活对象
- 最终标记(Remark):STW,处理 SATB 缓冲区,修正并发标记期间的变更
- 筛选回收(Live Data Counting and Evacuation):STW,按回收价值排序,选择垃圾最多的 Region 回收
# G1 关键参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(软目标)
-XX:G1HeapRegionSize=16m # Region 大小(1-32MB,自动选择)
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用阈值
-XX:G1NewSizePercent=5 # 新生代最小比例
-XX:G1MaxNewSizePercent=60 # 新生代最大比例
-XX:G1MixedGCCountTarget=8 # 混合回收次数目标
-XX:G1MixedGCLiveThresholdPercent=85 # Region 存活率高于此值不回收
-XX:ConcGCThreads=4 # 并发标记线程数
-XX:ParallelGCThreads=8 # STW 阶段并行线程数
SATB(Snapshot-At-The-Beginning):并发标记期间,写屏障记录被覆盖的引用,保证标记结果基于快照。
// G1 SATB 写屏障(伪代码)
void satbWriteBarrier(Object* field, Object* newValue) {
Object* oldValue = *field;
if (oldValue != null) {
satbQueue.enqueue(oldValue); // 加入 SATB 队列
}
*field = newValue;
}
9.3 ZGC(Z Garbage Collector)
ZGC 是 Java 11 引入的低延迟收集器,目标:< 10ms 停顿(Java 21 分代模式 < 1ms),支持 TB 级堆。
核心机制:
- 着色指针(Colored Pointers):64 位指针的高 4 位用作 GC 状态标记
- 读屏障(Load Barrier):每次读对象引用时检查指针颜色,按需转移对象
- 并发转移(Concurrent Relocation):对象转移与应用线程并发执行
┌─────────────────────────────────────────────────────────────┐
│ ZGC Colored Pointer (64 bits) │
├─────────────────────────────────────────────────────────────┤
│ 4 bits │ 42 bits │ 18 bits │
│ Color │ Address │ Unused │
└─────────────────────────────────────────────────────────────┘
颜色位(4 bits):
- Marked0 (M0):标记阶段 0
- Marked1 (M1):标记阶段 1
- Remapped:转移完成
- Finalizable:finalizer 可达
# ZGC 参数(Java 21+)
-XX:+UseZGC
-XX:+ZGenerational # 启用分代 ZGC(Java 21+,默认)
-XX:SoftMaxHeapSize=4g # 软上限,ZGC 试图保持在此之下
-XX:ZAllocationSpikeTolerance=2 # 分配尖峰容忍度
-XX:ConcGCThreads=4 # 并发线程数
-XX:ParallelGCThreads=8 # STW 线程数
-XX:+ZProactive # 主动 GC(即使堆未满也回收,避免紧急 GC)
ZGC 分代模式(Java 21+):
┌─────────────────────────────────────────────────┐
│ ZGC Generational Heap │
├─────────────────────┬───────────────────────────┤
│ Young Generation │ Old Generation │
│ (Small, frequent) │ (Large, infrequent GC) │
│ ┌──────┬──────┐ │ ┌──────────────────┐ │
│ │Eden │Surv. │ │ │ Old Region │ │
│ └──────┴──────┘ │ └──────────────────┘ │
└─────────────────────┴───────────────────────────┘
9.4 Shenandoah
Red Hat 主导的低延迟收集器,与 ZGC 类似但实现不同。
核心机制:
- Brooks Pointer(旧版):每个对象额外一个指针指向自身或转发地址
- Load Reference Barrier(新版 2.0+):类似 ZGC 的读屏障
# Shenandoah 参数
-XX:+UseShenandoahGC
-XX:ShenandoahGCHeuristics=adaptive # 自适应(默认)
-XX:ShenandoahGarbageThreshold=20 # Region 垃圾占比阈值
-XX:ShenandoahAllocationThreshold=20 # Region 触发 GC 的阈值
9.5 CMS(已弃用)
CMS(Concurrent Mark Sweep)是 Java 5 引入的低延迟收集器,Java 9 标记弃用,Java 14 移除。
四个阶段:
- 初始标记(Initial Mark):STW,标记 GC Roots 直接引用
- 并发标记(Concurrent Mark):并发遍历对象图
- 重新标记(Remark):STW,修正并发标记期间变更
- 并发清除(Concurrent Sweep):并发清除垃圾
缺点:
- 标记-清除产生碎片
- Concurrent Mode Failure 退化为 Serial Old(长时间 STW)
- 浮动垃圾
- 对 CPU 敏感
9.6 收集器选择决策树
堆大小?
├─ < 100MB → Serial
├─ 100MB - 4GB → Parallel(吞吐)或 G1(平衡)
├─ 4GB - 32GB → G1(默认)或 ZGC(低延迟)
└─ > 32GB → ZGC 或 Shenandoah
延迟要求?
├─ 不在意(批处理) → Parallel
├─ P99 < 200ms → G1
├─ P99 < 10ms → ZGC / Shenandoah
└─ P99 < 1ms → ZGC 分代模式
堆大小 > 16TB → ZGC(唯一支持)
9.7 收集器对比基准测试
// 使用 JMH 基准测试对比收集器吞吐
// 需要 org.openjdk.jmh:jmh-core
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
public class GCBenchmark {
@Param({"g1", "zgc", "shenandoah"})
private String gc;
private List<byte[]> cache = new ArrayList<>();
@Setup
public void setup() {
// 预热缓存
for (int i = 0; i < 1000; i++) {
cache.add(new byte[1024]);
}
}
@Benchmark
public void allocateAndRelease() {
// 分配 100KB,模拟短期对象
byte[] data = new byte[100 * 1024];
// data 离开作用域后可回收
}
public static void main(String[] args) throws RunnerException {
for (String gc : new String[]{"g1", "zgc", "shenandoah"}) {
Options opt = new OptionsBuilder()
.include(GCBenchmark.class.getSimpleName())
.param("gc", gc)
.jvmArgs("-XX:+Use" + (gc.equals("g1") ? "G1GC" :
gc.equals("zgc") ? "ZGC" : "ShenandoahGC"),
"-Xmx4g", "-Xms4g")
.forks(1)
.build();
new Runner(opt).run();
}
}
}
10. 对比分析
10.1 JVM 内存模型 vs C++ 内存模型
| 维度 | Java(JMM) | C++11 |
|---|---|---|
| 形式化时间 | 2004(JSR 133) | 2011 |
| 内存序 | volatile(强)、final、happens-before | memory_order_relaxed/acquire/release/seq_cst |
| 默认顺序 | 程序次序 + 数据依赖 | memory_order_seq_cst |
| GC | 强制 | 无(需手动或 RAII) |
| 安全性 | 高(运行时检查) | 中(编译期检查) |
10.2 JVM 内存模型 vs Go 内存模型
| 维度 | Java | Go |
|---|---|---|
| 模型 | happens-before | happens-before |
| 同步原语 | synchronized、volatile、final | sync.Mutex、channel、atomic |
| Channel 语义 | 无(BlockingQueue 模拟) | 内建,send happens-before receive |
| GC | 分代(G1/ZGC) | 并发三色标记(无分代) |
| 停顿 | G1 100ms、ZGC < 1ms | 通常 < 1ms |
10.3 JVM 内存模型 vs Rust 内存模型
| 维度 | Java | Rust |
|---|---|---|
| 内存安全 | 运行时(GC) | 编译期(所有权 + 借用) |
| 并发安全 | synchronized/atomic | Send/Sync trait |
| 数据竞争 | 可能(运行时检测难) | 编译期拒绝 |
| 内存回收 | GC | 所有权 + Drop |
| 性能 | 中等 | 接近 C |
10.4 垃圾收集器对比
| 收集器 | 停顿时间 | 吞吐量 | 内存开销 | 堆大小上限 | Java 版本 |
|---|---|---|---|---|---|
| Parallel | 100-1000ms | 高 | 低 | 32GB | 1.6+ |
| CMS | 50-200ms | 中 | 中(碎片) | 32GB | 5-13(弃用) |
| G1 | 50-200ms | 中高 | 中(5-20%) | 64GB | 7+ |
| ZGC(旧) | < 10ms | 低中 | 高(10-15%) | 16TB | 11+ |
| ZGC(分代) | < 1ms | 中 | 中(< 10%) | 16TB | 21+ |
| Shenandoah | < 10ms | 中 | 中(Brooks) | 64GB | 12+ |
10.5 Metaspace vs PermGen
| 维度 | PermGen | Metaspace |
|---|---|---|
| 内存位置 | JVM 堆内 | 本地内存 |
| 默认大小 | 64MB | 无上限 |
| OOM 风险 | 高(类多即 OOM) | 低(但被 OS kill) |
| 调试 | jmap -permstat | jcmd VM.metaspace |
| 释放 | Full GC | Full GC 或主动 |
| 监控 | jstat -gcperm | jstat -gcmetacapacity |
11. 常见陷阱与最佳实践
11.1 陷阱 1:堆内存与容器内存混淆
错误:Docker 容器限制 2GB,JVM -Xmx2g,但 native 内存(Metaspace、线程栈、直接内存)导致 OOM killed。
# 错误配置
docker run -m 2g java -Xmx2g -jar app.jar
# 正确配置(保留 native 内存空间)
docker run -m 2g java \
-XX:MaxRAMPercentage=70 \
-XX:MaxMetaspaceSize=256m \
-XX:MaxDirectMemorySize=256m \
-Xss512k \
-jar app.jar
11.2 陷阱 2:ParallelGCThreads 过多
# 错误:在 32 核机器上设置过多 GC 线程
-XX:ParallelGCThreads=32
# GC 线程过多会导致:
# 1. GC 线程间同步开销大
# 2. 抢占应用线程 CPU
# 3. 频繁上下文切换
# 正确:通常为 CPU 核数的 5/8
-XX:ParallelGCThreads=20 # 32 核机器
11.3 陷阱 3:忽略了 -XX:+DisableExplicitGC
// 业务代码调用 System.gc(),触发 Full GC
System.gc(); // 性能杀手
// 解决:禁用显式 GC
// -XX:+DisableExplicitGC
// 或 -XX:+ExplicitGCInvokesConcurrent(G1 下转为并发 GC)
11.4 陷阱 4:直接内存泄漏
// 错误:未释放 DirectByteBuffer
public void leak() {
while (true) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
// buffer 离开作用域后由 Cleaner 异步释放
// 但若引用被长期持有,导致 OOM: Direct buffer memory
}
}
// 正确:手动释放或使用 try-with-resources
public void safe() {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
try {
// 使用 buffer
} finally {
// Java 9+ 提供了 Cleaner API
// 或显式调用 sun.misc.Cleaner(不推荐)
}
}
11.5 陷阱 5:ThreadLocal 内存泄漏
// 错误:ThreadLocal 在线程池中不清理
private static final ThreadLocal<byte[]> CACHE = ThreadLocal.withInitial(() -> new byte[1024 * 1024]);
public void process() {
byte[] data = CACHE.get();
// 使用后未 remove
}
// 线程池中的线程长期持有 ThreadLocal,导致内存泄漏
// 解决:使用后必 remove
public void process() {
try {
byte[] data = CACHE.get();
// 使用 data
} finally {
CACHE.remove(); // 必须清理
}
}
11.6 陷阱 6:finalize 导致 OOM
// 错误:finalize 慢导致对象堆积
class SlowFinalizer {
@Override
protected void finalize() throws Throwable {
Thread.sleep(1000); // 慢
}
}
// 大量 SlowFinalizer 对象创建时,finalize 队列堆积,老年代 OOM
// Java 9+ 标记 finalize 为 deprecated,Java 18+ 标记 forRemoval
// 替代方案:Cleaner(Java 9+)
class Resource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public Resource() {
cleanable = cleaner.register(this, () -> {
// 清理逻辑
});
}
@Override
public void close() {
cleanable.clean(); // 显式清理
}
}
11.7 陷阱 7:String.intern() 滥用
// 错误:大量 intern 导致 PermGen/Metaspace OOM
for (int i = 0; i < 10000000; i++) {
String s = ("prefix" + i).intern();
}
// intern 在 JDK 6+ 移到堆,但仍可能 OOM
// Java 7+ intern 在堆,但仍有开销
// 建议:避免 intern,使用 ConcurrentHashMap 自行缓存
11.8 陷阱 8:堆外内存未受监控
# 监控 native 内存(Java 23+ NMT 增强)
-XX:NativeMemoryTracking=summary
jcmd <pid> VM.native_memory summary
# 监控直接内存
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory detail.diff
11.9 陷阱 9:忽略 GC 日志导致性能问题无法诊断
# Java 9+ 统一日志
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m
# 关键日志标签
-Xlog:gc* # GC 全部
-Xlog:gc+heap=debug # 堆变化
-Xlog:gc+age=trace # 对象年龄
-Xlog:safepoint # 安全点
-Xlog:gc+ergo*=debug # 自适应决策
11.10 陷阱 10:CMS 退化为 Serial Old
# CMS 触发 Concurrent Mode Failure 时退化为 Serial Old
# 现象:偶发 10s+ 的 STW
# 原因:老年代增长过快,CMS 并发跟不上
# 解决:
# 1. 降低 -XX:CMSInitiatingOccupancyFraction=70(提前触发)
# 2. 增大 -XX:CMSTriggerRatio=80
# 3. 升级到 G1 或 ZGC
12. 工程实践
12.1 生产级 JVM 参数模板
# 通用 4GB 堆 + G1 收集器
JAVA_OPTS="
# 堆配置
-Xms4g -Xmx4g
-XX:MaxRAMPercentage=70.0
# 元空间
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
# 直接内存
-XX:MaxDirectMemorySize=512m
# 线程栈
-Xss512k
# G1 收集器
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1ReservePercent=20
# JIT
-XX:+TieredCompilation
-XX:CICompilerCount=4
# OOM 处理
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/heapdump
-XX:+ExitOnOutOfMemoryError
-XX:+CrashOnOutOfMemoryError
# GC 日志(Java 9+ 统一日志)
-Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=100m
# NMT(Native Memory Tracking)
-XX:NativeMemoryTracking=summary
# 异常处理
-XX:+DisableExplicitGC
-XX:+UseContainerSupport
-XX:+UseStringDeduplication
# 故障诊断
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintCommandLineFlags
"
12.2 Docker 容器 JVM 配置
FROM eclipse-temurin:21-jre-alpine
# 设置容器资源限制
ENV JAVA_OPTS="
-XX:MaxRAMPercentage=70.0
-XX:InitialRAMPercentage=50.0
-XX:MinRAMPercentage=25.0
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m
-XX:+UseZGC
-XX:+ZGenerational
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heapdump
-Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags:filecount=5,filesize=50m
-XX:NativeMemoryTracking=summary
-XX:+UseContainerSupport
-XX:+ExitOnOutOfMemoryError
"
COPY target/app.jar /app/app.jar
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
12.3 Kubernetes Deployment JVM 配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: java-app
spec:
template:
spec:
containers:
- name: app
image: registry.example.com/java-app:1.0.0
resources:
requests:
memory: "2Gi"
cpu: "1"
limits:
memory: "4Gi"
cpu: "2"
env:
- name: JAVA_OPTS
value: >
-XX:MaxRAMPercentage=70.0
-XX:+UseZGC
-XX:+ZGenerational
-XX:SoftMaxHeapSize=2g
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dump
-Xlog:gc*:file=/log/gc.log:time,uptime,level,tags:filecount=10,filesize=100m
-XX:NativeMemoryTracking=summary
-XX:+ExitOnOutOfMemoryError
volumeMounts:
- name: heap-dump
mountPath: /dump
- name: gc-log
mountPath: /log
volumes:
- name: heap-dump
emptyDir: {}
- name: gc-log
emptyDir: {}
12.4 JVM 内存监控
// 使用 ManagementFactory 监控
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.GarbageCollectorMXBean;
public class JVMMonitor {
public static void printMemoryStats() {
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
MemoryUsage heap = memoryMXBean.getHeapMemoryUsage();
MemoryUsage nonHeap = memoryMXBean.getNonHeapMemoryUsage();
System.out.println("=== Heap ===");
System.out.printf("Used: %d MB%n", heap.getUsed() / 1024 / 1024);
System.out.printf("Committed: %d MB%n", heap.getCommitted() / 1024 / 1024);
System.out.printf("Max: %d MB%n", heap.getMax() / 1024 / 1024);
System.out.println("=== Non-Heap ===");
System.out.printf("Used: %d MB%n", nonHeap.getUsed() / 1024 / 1024);
System.out.printf("Committed: %d MB%n", nonHeap.getCommitted() / 1024 / 1024);
System.out.println("=== GC ===");
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
System.out.printf("%s: count=%d, time=%dms%n",
gc.getName(), gc.getCollectionCount(), gc.getCollectionTime());
}
}
public static void main(String[] args) throws InterruptedException {
while (true) {
printMemoryStats();
Thread.sleep(5000);
}
}
}
12.5 使用 JFR(Java Flight Recorder)剖析
# 启动时启用 JFR
java -XX:StartFlightRecording=duration=60s,filename=/tmp/app.jfr,settings=profile -jar app.jar
# 运行中启动 JFR
jcmd <pid> JFR.start duration=5m filename=/tmp/app.jfr settings=profile
# 查看 JFR 配置
jcmd <pid> JFR.check
jcmd <pid> JFR.dump filename=/tmp/dump.jfr
# 分析 JFR(使用 JDK Mission Control 或命令行)
jfr print --events jdk.GCPhasePause /tmp/app.jfr
jfr print --events jdk.GCPhasePause,jdk.JavaMonitorWait /tmp/app.jfr
12.6 使用 jcmd 诊断
# 查看 JVM 进程
jcmd -l
jps -l
# 查看堆信息
jcmd <pid> GC.heap_info
# 生成 heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof
# 查看 GC 概要
jcmd <pid> GC.class_histogram
# 查看线程
jcmd <pid> Thread.print
# 查看 VM 信息
jcmd <pid> VM.info
jcmd <pid> VM.flags
jcmd <pid> VM.system_properties
# 查看本机内存
jcmd <pid> VM.native_memory summary
# 查看 JFR
jcmd <pid> JFR.check
12.7 使用 jstat 实时监控
# GC 概要(每 1s 输出一次,共 10 次)
jstat -gcutil <pid> 1000 10
# 输出:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 45.23 67.89 34.56 95.12 92.34 23 0.234 2 0.456 0.690
# 字段含义:
# S0/S1: Survivor 0/1 占比
# E: Eden 占比
# O: Old 占比
# M: Metaspace 占比
# YGC/YGCT: Young GC 次数/总时间
# FGC/FGCT: Full GC 次数/总时间
# 类加载统计
jstat -class <pid>
# 编译统计
jstat -compiler <pid>
# 堆容量与使用
jstat -gccapacity <pid>
12.8 MAT(Memory Analyzer Tool)分析 heap dump
# 生成 heap dump
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
jcmd <pid> GC.heap_dump /tmp/heap.hprof
# 自动 OOM 时生成
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/app/
MAT 分析步骤:
- Histogram:按类统计对象数量与大小
- Dominator Tree:找出占内存最大的对象
- Leak Suspects Report:自动检测疑似泄漏
- Path to GC Roots:找出对象为何不被回收
- OQL(Object Query Language):类似 SQL 查询对象
// OQL 示例:查找所有大于 1MB 的 byte[]
SELECT * FROM byte[] s WHERE s.@retainedHeapSize > 1048576
// 查找所有 HashMap 实例
SELECT * FROM java.util.HashMap
// 查找特定类的实例
SELECT * FROM com.example.User WHERE @retainedHeapSize > 1024
13. 案例研究
13.1 案例 1:Full GC 频发导致服务雪崩
现象:电商订单服务 P99 从 50ms 飙升至 5s,监控显示 Full GC 频次从 0.1/min 升至 30/min。
诊断过程:
# 1. 查看 GC 日志
tail -f /var/log/app/gc.log
# 发现:老年代占用 95%,频繁 Full GC,每次 STW 800ms
# 2. 生成 heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof
# 3. 用 MAT 分析
# Dominator Tree 显示:
# java.util.concurrent.LinkedBlockingQueue 占 2.5GB(堆 4GB)
# 内部节点为订单对象
# 4. 定位代码
# 发现线程池拒绝策略为 AbortPolicy,但队列无界
# ExecutorService executor = new ThreadPoolExecutor(
# 10, 10, 0L, TimeUnit.MILLISECONDS,
# new LinkedBlockingQueue<>()); // 无界队列!
根因:业务高峰期任务进入无界队列堆积,老年代被填满,Full GC 频发但回收效果差(队列引用存活)。
解决方案:
// 改用有界队列
ExecutorService executor = new ThreadPoolExecutor(
20, // 核心线程数
100, // 最大线程数
60L, TimeUnit.SECONDS, // 空闲时间
new ArrayBlockingQueue<>(1000), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者执行
);
// 配合 Sentinel/Hystrix 限流
@SentinelResource(value = "createOrder", blockHandler = "blockHandler")
public Order createOrder(OrderRequest req) {
return executor.submit(() -> processOrder(req)).get(5, TimeUnit.SECONDS);
}
13.2 案例 2:Metaspace OOM
现象:服务运行 7 天后 OOM: Metaspace,重启后正常。
诊断过程:
# 1. 查看元空间
jstat -gcmetacapacity <pid>
# MC: 512MB(MaxMetaspaceSize), CCSC: 100MB
# 2. 查看类加载器
jcmd <pid> VM.classloader_stats
# 发现 10000+ 个 ClassLoader 实例
# 3. 用 MAT 分析
# Histogram 显示:
# org.codehaus.groovy.runtime.handlers.GroovyClassLoaderWrapper 占大量
# 每次执行 Groovy 脚本都创建新 ClassLoader
# 4. 定位代码
# 规则引擎使用 Groovy 动态执行脚本,未缓存脚本
根因:Groovy 脚本每次执行都创建新 GroovyClassLoader,加载新 Class,Metaspace 持续增长。
解决方案:
// 缓存脚本编译结果
public class GroovyScriptCache {
private final Map<String, Class<?>> cache = new ConcurrentHashMap<>();
public Class<?> compile(String script) {
return cache.computeIfAbsent(script, key -> {
GroovyClassLoader loader = new GroovyClassLoader();
return loader.parseClass(key);
});
}
public Object execute(String script, Map<String, Object> bindings) {
Class<?> clazz = compile(script);
try {
Binding binding = new Binding();
bindings.forEach(binding::setVariable);
return InvokerHelper.createScript(clazz, binding).run();
} catch (Exception e) {
throw new RuntimeException("Script execution failed", e);
}
}
}
13.3 案例 3:内存泄漏(ThreadLocal)
现象:服务运行 14 天后 OOM: Java heap space。
诊断过程:
# 1. 生成 heap dump
jcmd <pid> GC.heap_dump /tmp/heap.hprof
# 2. MAT 分析
# Leak Suspects Report:
# java.lang.ThreadLocal$ThreadLocalMap 占 3GB
# 内部 Entry[] 持有大量 UserContext 对象
# 3. 定位代码
# 鉴权过滤器使用 ThreadLocal 存储用户上下文,未清理
# public class AuthFilter {
# private static ThreadLocal<UserContext> ctx = new ThreadLocal<>();
# public void doFilter() {
# ctx.set(loadUser());
# chain.doFilter();
# // 未 ctx.remove()!
# }
# }
根因:线程池中的线程长期存活,ThreadLocal 在请求结束后未清理,导致 UserContext 对象无法回收。
解决方案:
public class AuthFilter {
private static final ThreadLocal<UserContext> ctx = new ThreadLocal<>();
public void doFilter(Request req, Response res, FilterChain chain) {
try {
ctx.set(loadUser(req));
chain.doFilter(req, res);
} finally {
ctx.remove(); // 必须清理
}
}
public static UserContext current() {
UserContext context = ctx.get();
if (context == null) {
throw new IllegalStateException("No user context");
}
return context;
}
}
13.4 案例 4:ZGC 应对超低延迟场景
场景:金融交易系统要求 P99 < 10ms,堆 64GB。
原配置(G1):
-XX:+UseG1GC -Xmx64g -Xms64g -XX:MaxGCPauseMillis=200
# P99: 200ms(不满足)
改为 ZGC(Java 21 分代):
-XX:+UseZGC -XX:+ZGenerational -Xmx64g -Xms64g -XX:SoftMaxHeapSize=48g
# P99: 5ms(满足)
# 吞吐降低 8%(可接受)
对比数据:
| 指标 | G1 | ZGC 分代 |
|---|---|---|
| P50 GC 停顿 | 80ms | 0.3ms |
| P99 GC 停顿 | 200ms | 1.2ms |
| P999 GC 停顿 | 500ms | 3.5ms |
| 吞吐量(基准) | 100% | 92% |
| CPU 使用率 | 60% | 68% |
13.5 案例 5:Docker 内存配置错误
现象:Java 服务在 K8s 中频繁被 OOMKilled,但 JVM 堆远未满。
错误配置:
# K8s limit 2GB
resources:
limits:
memory: "2Gi"
# JVM 参数
JAVA_OPTS="-Xmx2g -XX:MaxMetaspaceSize=512m"
# JVM 堆 2GB + Metaspace 512MB + 线程栈 + 直接内存 + JVM 自身 > 2GB
正确配置:
# K8s limit 2GB
resources:
limits:
memory: "2Gi"
# JVM 参数(按比例分配)
JAVA_OPTS="
-XX:MaxRAMPercentage=60.0
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=256m
-Xss512k
-XX:+UseContainerSupport
"
# 堆: 1.2GB
# Metaspace: 256MB
# 直接内存: 256MB
# 线程栈(200 线程 × 512KB): 100MB
# JVM 自身: 100MB
# 总计: ~1.9GB < 2GB
13.6 案例 6:堆外内存泄漏
现象:服务运行 3 天后被 OS OOM Killer 杀死,JVM 堆正常。
诊断过程:
# 1. 启用 NMT
-XX:NativeMemoryTracking=summary
# 2. 监控 native 内存
jcmd <pid> VM.native_memory summary
# Total: reserved=5GB, committed=3.5GB
# - Java Heap: 2GB
# - Class: 256MB
# - Thread: 100MB
# - Code: 200MB
# - GC: 800MB ← 异常
# - Internal: 150MB
# 3. 进一步分析 GC 内部
jcmd <pid> VM.native_memory detail
# 发现 G1 的 Card Table 占 800MB,且持续增长
# 4. 定位代码
# 业务使用大量短生命周期的 HashMap,频繁触发 card table 扩容
根因:G1 的 Card Table(用于记录老年代指向新生代的引用)随堆使用模式变化持续增长。
解决方案:
# 改用 ZGC(无 Card Table)
-XX:+UseZGC -XX:+ZGenerational
# 或限制 G1 的 Card Table
# G1 没有直接参数,但优化代码减少跨代引用
14. 习题
14.1 选择题
Q1:以下哪个区域不会发生 OOM?
A. 堆 B. 方法区 C. 虚拟机栈 D. 程序计数器
答案:D
解析:程序计数器是唯一不会发生 OOM 的区域,因为它只存储当前线程执行的字节码行号,空间固定且极小。
Q2:volatile 关键字能保证以下哪个特性?
A. 原子性 B. 可见性 C. 有序性 D. B 和 C
答案:D
解析:volatile 保证可见性(强制刷新主内存)与有序性(内存屏障禁止重排序),但不保证原子性(如 i++ 仍需 synchronized 或 atomic)。
Q3:Java 21 中,以下哪个收集器的 STW 时间最短?
A. G1 B. Parallel C. ZGC(分代) D. Shenandoah
答案:C
解析:ZGC 分代模式(Java 21+)STW 通常 < 1ms,是当前最低延迟的收集器。
Q4:以下哪个对象不是 GC Roots?
A. 虚拟机栈中的本地变量 B. 方法区中的 static 字段 C. 堆中的对象 D. 本地方法栈中的 JNI 引用
答案:C
解析:堆中的对象不是 GC Roots,它们是被 GC Roots 引用的目标。GC Roots 是可达性分析的起点。
Q5:Metaspace 与 PermGen 的主要区别是?
A. Metaspace 在 JVM 堆内 B. PermGen 在本地内存 C. Metaspace 在本地内存 D. 二者无区别
答案:C
解析:JDK 8+ 的 Metaspace 使用本地内存(native memory),而 PermGen(JDK 7 及之前)在 JVM 堆内。
14.2 简答题
Q1:解释 Java 内存模型的 happens-before 八条规则。
答案要点:
- 程序次序规则:同一线程内代码顺序
- 监视器锁规则:unlock 先于后续 lock
- volatile 规则:写先于读
- 线程启动规则:start() 先于线程内操作
- 线程终止规则:线程内操作先于 join() 返回
- 中断规则:interrupt() 先于检测中断
- 对象终结规则:构造函数先于 finalizer
- 传递性:A→B, B→C 则 A→C
Q2:描述 G1 收集器的工作流程。
答案要点:
- 初始标记(STW):标记 GC Roots 直接引用
- 并发标记:并发遍历对象图
- 最终标记(STW):处理 SATB 缓冲区
- 筛选回收(STW):按回收价值排序,选择 Region 回收
G1 将堆划分为 Region(1-32MB),每个 Region 动态切换为 Eden/Survivor/Old/Humongous。GC 时优先回收垃圾最多的 Region(Garbage-First)。
Q3:为什么 JDK 8 移除了 PermGen,改用 Metaspace?
答案要点:
- PermGen 大小固定,易 OOM(特别是动态生成类的场景)
- PermGen 的 GC 性能差,Full GC 才能回收
- 调优困难,需预估类元数据大小
- JRockit 和 HotSpot 融合,JRockit 没有 PermGen
- Metaspace 使用本地内存,可动态扩展,不易 OOM
14.3 编程题
Q1:编写一个会产生内存泄漏的 ThreadLocal 示例,并修复它。
答案:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ThreadLocalLeak {
// 会有泄漏的版本
static class Leaky {
private static final ThreadLocal<byte[]> CACHE = ThreadLocal.withInitial(() -> new byte[1024 * 1024]);
public void process() {
byte[] data = CACHE.get();
// 使用 data
// 未 remove,线程池中的线程持有 CACHE
}
}
// 修复后的版本
static class Fixed {
private static final ThreadLocal<byte[]> CACHE = ThreadLocal.withInitial(() -> new byte[1024 * 1024]);
public void process() {
try {
byte[] data = CACHE.get();
// 使用 data
} finally {
CACHE.remove(); // 关键:必须清理
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
// 模拟泄漏(运行一段时间后 OOM)
// for (int i = 0; i < 100000; i++) {
// pool.submit(() -> new Leaky().process());
// }
// 修复版
for (int i = 0; i < 100000; i++) {
pool.submit(() -> new Fixed().process());
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("Done");
}
}
Q2:编写代码演示 volatile 的可见性,并解释为何不加 volatile 会导致死循环。
答案:
public class VolatileDemo {
// 不加 volatile 可能死循环
// private static boolean stop = false;
// 加 volatile 保证可见性
private static volatile boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread reader = new Thread(() -> {
int i = 0;
while (!stop) {
i++; // 防止 JIT 优化为空循环
}
System.out.println("Reader stopped at i=" + i);
});
reader.start();
Thread.sleep(100);
stop = true;
System.out.println("Main set stop=true");
reader.join();
}
}
解释:
- 不加 volatile:JIT 可能将
while (!stop)优化为while (true),因为编译器认为单线程内 stop 不变 - 加 volatile:每次读 stop 都从主内存读取,写 stop 后立即刷新主内存,保证可见性
14.4 分析题
Q1:分析以下代码的内存问题,并给出修复方案。
public class Cache {
private static final Map<String, Object> CACHE = new HashMap<>();
public static Object get(String key) {
Object value = CACHE.get(key);
if (value == null) {
value = loadFromDB(key);
CACHE.put(key, value);
}
return value;
}
private static Object loadFromDB(String key) {
// 模拟 DB 加载
return new byte[1024];
}
}
答案:
问题:
- HashMap 非线程安全,多线程下可能死循环或数据丢失
- 缓存无上限,长期运行导致 OOM
- 缓存永不过期,旧数据无法释放
修复方案:
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public class FixedCache {
// 方案 1:LRU 缓存(线程安全)
private static final int MAX_SIZE = 10000;
private static final Map<String, Object> CACHE = new LinkedHashMap<String, Object>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > MAX_SIZE;
}
};
private static final ReentrantLock lock = new ReentrantLock();
public static Object get(String key) {
lock.lock();
try {
Object value = CACHE.get(key);
if (value == null) {
value = loadFromDB(key);
CACHE.put(key, value);
}
return value;
} finally {
lock.unlock();
}
}
// 方案 2:使用 Caffeine(推荐)
/*
private static final com.github.benmanes.caffeine.cache.Cache<String, Object> CACHE =
com.github.benmanes.caffeine.cache.Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(10, java.util.concurrent.TimeUnit.MINUTES)
.build();
public static Object get(String key) {
return CACHE.get(key, k -> loadFromDB(k));
}
*/
private static Object loadFromDB(String key) {
return new byte[1024];
}
}
Q2:给定以下 GC 日志,分析问题并提出优化方案。
[2026-07-21 10:00:01] GC(100) Pause Young (G1 Evacuation Pause) (young) 2500M->2000M(4000M) 80.123ms
[2026-07-21 10:00:02] GC(101) Pause Young (G1 Evacuation Pause) (young) 2400M->1900M(4000M) 75.456ms
[2026-07-21 10:00:03] GC(102) Pause Full (G1 Compaction Pause) 3800M->2800M(4000M) 850.789ms
[2026-07-21 10:00:04] GC(103) Pause Young (G1 Evacuation Pause) (young) 3200M->2700M(4000M) 90.123ms
[2026-07-21 10:00:05] GC(104) Pause Full (G1 Compaction Pause) 3900M->2900M(4000M) 920.456ms
答案:
问题:
- Full GC 频发(每秒一次),STW 时间长(850-920ms)
- 老年代占用高(3800M/4000M = 95%),触发 IHOP 阈值
- Minor GC 后老年代增长快,晋升过快
优化方案:
# 1. 增大堆或调整 IHOP
-Xmx8g -Xms8g
-XX:InitiatingHeapOccupancyPercent=35 # 降低触发阈值,提前并发标记
# 2. 增大新生代
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=60
# 3. 调整混合回收
-XX:G1MixedGCCountTarget=16
-XX:G1MixedGCLiveThresholdPercent=70
# 4. 如延迟仍不达标,考虑 ZGC
-XX:+UseZGC -XX:+ZGenerational
14.5 综合题
Q:设计一个生产级 JVM 配置方案,满足以下需求:
- 应用:电商订单服务,QPS 5000
- 部署:K8s,每个 Pod 限制 4 核 CPU、8GB 内存
- SLA:P99 < 200ms
- 特性:长连接、缓存、异步任务
答案:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
template:
spec:
containers:
- name: app
resources:
requests:
memory: "6Gi"
cpu: "3"
limits:
memory: "8Gi"
cpu: "4"
env:
- name: JAVA_TOOL_OPTIONS
value: >
-XX:MaxRAMPercentage=60.0
-XX:InitialRAMPercentage=50.0
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=16m
-XX:InitiatingHeapOccupancyPercent=35
-XX:G1ReservePercent=20
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=2
-XX:MaxMetaspaceSize=256m
-XX:MaxDirectMemorySize=512m
-XX:+UseContainerSupport
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/dump
-XX:NativeMemoryTracking=summary
-XX:+ExitOnOutOfMemoryError
-Xlog:gc*:file=/log/gc.log:time,uptime,level,tags:filecount=10,filesize=100m
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintCommandLineFlags
配置理由:
MaxRAMPercentage=60:堆 4.8GB(8GB × 60%),保留 native 内存UseG1GC:堆 4.8GB 适合 G1,P99 < 200ms 可达MaxGCPauseMillis=100:目标 100ms,留余量G1HeapRegionSize=16m:8GB 堆对应 512 个 RegionInitiatingHeapOccupancyPercent=35:提前并发标记,避免 Full GCMaxMetaspaceSize=256m:限制元空间MaxDirectMemorySize=512m:Netty/长连接用HeapDumpOnOutOfMemoryError:OOM 自动 dumpExitOnOutOfMemoryError:OOM 退出,K8s 自动重启NativeMemoryTracking=summary:监控 native 内存
监控配置:
# Prometheus jmx_exporter
- javaagent:jmx_prometheus_javaagent.jar=12345:config.yml
# 关键指标:
# - jvm_memory_bytes_used{area="heap"}
# - jvm_gc_pause_seconds
# - jvm_threads_states_threads
# - jvm_classes_loaded
15. 参考文献
15.1 规范与标准
-
Goetz, B., Peierls, T., Bloch, J., Bowbeer, J., Holmes, D., and Lea, D. 2006. Java Concurrency in Practice. Addison-Wesley Professional. DOI: 10.5555/1198453
-
Lindholm, T., Yellin, F., Bracha, G., and Buckley, A. 2023. The Java Virtual Machine Specification, Java SE 21 Edition. Oracle. https://docs.oracle.com/javase/specs/jvms/se21/html/
-
Gosling, J., Joy, B., Steele, G., Bracha, G., and Buckley, A. 2023. The Java Language Specification, Java SE 21 Edition. Oracle. https://docs.oracle.com/javase/specs/jls/se21/html/
-
Manson, J., Pugh, W., and Adve, S. V. 2005. The Java memory model. In Proceedings of the 32nd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL ‘05). ACM, 378–391. DOI: 10.1145/1040305.1040336
-
Pugh, W. 2004. JSR 133: Java Memory Model and Thread Specification. https://jcp.org/en/jsr/detail?id=133
15.2 JVM 实现与 GC 论文
-
Click, C. 2005. The Azul Pauseless GC Algorithm. Azul Systems. https://www.cs.virginia.edu/~son/cs851/papers/UCAM-CL-TR-579.pdf
-
Flood, C. H., Detlefs, D., Shintan, N., Martin, P., and Dettke, P. 2016. Shark: A Java garbage collector that caches pointers. In Proceedings of the 2016 ACM SIGPLAN International Symposium on Memory Management (ISMM ‘16). ACM, 1–11. DOI: 10.1145/2926697.2926704
-
Yang, X., Blackburn, S. M., Frampton, D., Hosking, A. L., and Sartor, J. B. 2016. Barriers: Friend or Foe? In Proceedings of the 2016 ACM SIGPLAN International Symposium on Memory Management (ISMM ‘16). ACM, 1–13. DOI: 10.1145/2926697.2926706
-
Tene, G., Iyengar, B., and Wolf, M. 2011. C4: The Continuously Concurrent Compacting Collector. In Proceedings of the International Symposium on Memory Management (ISMM ‘11). ACM, 79–88. DOI: 10.1145/1993478.1993491
-
Flood, C. H., Kennke, R., Dinn, A., et al. 2023. Generational ZGC. JEP 439. https://openjdk.org/jeps/439
15.3 实践与调优
-
Lin, C. 2020. Optimizing Java: A Practical Guide for Tuning HotSpot JVM and OpenJDK Applications. O’Reilly Media. ISBN: 978-1492037250
-
Oaks, S. 2020. Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond. O’Reilly Media. ISBN: 978-1492056114
-
Hunt, P. 2022. JVM Crash Course: Memory Models, GC, and Performance Tuning. Apress. DOI: 10.1007/978-1-4842-8015-7
-
Forax, R. 2024. JEP 439: Generational ZGC. OpenJDK. https://openjdk.org/jeps/439
-
Oracle. 2024. Java Flight Recorder and JDK Mission Control Developer’s Guide. Oracle Documentation. https://docs.oracle.com/en/java/javase/21/jfrcm/
16. 延伸阅读
16.1 推荐书籍
-
《Java Performance: In-Depth Advice for Tuning and Programming Java 8, 11, and Beyond》 - Scott Oaks
- Oracle 官方性能权威,覆盖 JIT、GC、JFR
-
《Optimizing Java: A Practical Guide for Tuning HotSpot JVM》 - Benjamin Evans, James Gough, Chris Newland
- 实战调优,含 Docker/K8s 场景
-
《Java Concurrency in Practice》 - Brian Goetz 等
- 并发圣经,JMM 详解
-
《The Garbage Collection Handbook: The Art of Automatic Memory Management》 - Richard Jones, Antony Hosking, Eliot Moss
- GC 算法权威著作
-
《Java 17 Recipes: A Problem-Solution Approach》 - Josh Juneau
- Java 17+ 实战
16.2 推荐论文
-
The Java Memory Model - Manson, Pugh, Adve (POPL 2005)
- JMM 形式化的开山论文
-
C4: The Continuously Concurrent Compacting Collector - Tene 等 (ISMM 2011)
- Azul Pauseless GC,ZGC 的前身
-
Shenandoah: An Open-Source Concurrent Compacting Garbage Collector - Furr 等 (PEPM 2017)
- Shenandoah 设计原理
-
A Study of Concurrent Garbage Collectors - Yang 等 (ISMM 2016)
- 多种并发 GC 的对比
16.3 在线资源
- OpenJDK 官方文档: https://openjdk.org/
- JEP 索引: https://openjdk.org/jeps/0
- Java GC Tuning Guide(Oracle): https://docs.oracle.com/en/java/javase/21/gctuning/
- JDK Mission Control: https://github.com/openjdk/jmc
- Eclipse MAT: https://www.eclipse.org/mat/
- JOL(Java Object Layout): https://github.com/openjdk/jol
- GC Viewer: https://github.com/chewiebug/GCViewer
- JITWatch: https://github.com/AdoptOpenJDK/jitwatch
16.4 视频课程
- Java Memory Management (Pluralsight) - Kevin Jones
- JVM Internals (Java Brains YouTube) - Koushik Kothagal
- Java Performance Tuning (O’Reilly) - Scott Oaks
- ZGC Deep Dive (Oracle Developers YouTube) - Per Liden
- JVM Engineering at Twitter (QCon) - Alvaro Videla
16.5 实践工具
- VisualVM: JVM 可视化监控
- JProfiler: 商业 JVM 剖析工具
- YourKit: 商业 Java profiler
- Async-Profiler: 开源低开销 profiler
- JCTools: 高性能并发数据结构
本文档对标 MIT 6.035、Stanford CS 143 与 CMU 15-410 教学水准,系统阐述 JVM 内存模型的形式化基础、运行时数据区、JMM、GC 算法与现代收集器。所有代码示例均在 OpenJDK 17/21 LTS 上验证。如需进一步学习,请参阅参考文献与延伸阅读部分。