📖目录
- 引言:JVM的"身体"与"心脏"
- [1. JDK 7的JVM结构及GC算法:永久代的烦恼](#1. JDK 7的JVM结构及GC算法:永久代的烦恼)
-
- [1.1 JDK 7的JVM身体结构](#1.1 JDK 7的JVM身体结构)
- [1.2 JDK 7的GC算法:老式发动机](#1.2 JDK 7的GC算法:老式发动机)
- [2. JDK 21的JVM结构及GC算法:元空间的革命](#2. JDK 21的JVM结构及GC算法:元空间的革命)
-
- [2.1 JDK 21的JVM身体结构](#2.1 JDK 21的JVM身体结构)
- [2.2 JDK 21的GC算法:现代低延迟发动机](#2.2 JDK 21的GC算法:现代低延迟发动机)
- [3. JDK 7 vs JDK 21:JVM结构与GC算法对比](#3. JDK 7 vs JDK 21:JVM结构与GC算法对比)
- [4. 现代版本推荐的GC算法:为什么选择它们?](#4. 现代版本推荐的GC算法:为什么选择它们?)
-
- [4.1 G1 GC:平衡之王](#4.1 G1 GC:平衡之王)
- [4.2 ZGC:低延迟的王者](#4.2 ZGC:低延迟的王者)
- [4.3 Shenandoah GC:ZGC的替代品](#4.3 Shenandoah GC:ZGC的替代品)
- [5. 源码解析:GC算法的执行流程](#5. 源码解析:GC算法的执行流程)
-
- [5.1 G1 GC的核心流程](#5.1 G1 GC的核心流程)
- [5.2 ZGC的核心流程](#5.2 ZGC的核心流程)
- [5.3 源码解析:ZGC的着色指针实现](#5.3 源码解析:ZGC的着色指针实现)
- 六、实战:GC算法选择与配置
-
- [6.1 选择GC算法的简单指南](#6.1 选择GC算法的简单指南)
- [6.2 实际应用中的GC配置](#6.2 实际应用中的GC配置)
- [7. 结论:JVM的进化之路](#7. 结论:JVM的进化之路)
- [8. 经典书籍推荐](#8. 经典书籍推荐)
- [9. 写在最后](#9. 写在最后)
引言:JVM的"身体"与"心脏"
想象一下,你有一辆汽车,它需要定期保养才能跑得更远、更稳。JVM就是Java程序的"汽车",而GC(垃圾回收)则是它的"发动机保养系统"。JDK 7和JDK 21就像这辆汽车的两个不同版本------一个用的是老式发动机,一个用的是最新科技。
JVM的内存结构是它的"身体",而GC算法是它的"心脏",负责清理"垃圾"(不再使用的对象)。随着Java的发展,JVM的身体结构和心脏功能都在不断进化,让Java应用更高效、更稳定。
1. JDK 7的JVM结构及GC算法:永久代的烦恼
1.1 JDK 7的JVM身体结构
在JDK 7中,JVM的内存结构就像一个老式仓库,分为几个固定区域:
┌───────────────────────────────┐
│ JVM内存结构 │
├─────────────┬───────────────┤
│ 方法区(PermGen) │ 堆内存 │
│ │ ┌───────────┐ │
│ │ │ 新生代 │ │
│ │ │ ├───────┤ │
│ │ │ │ Eden │ │
│ │ │ ├───────┤ │
│ │ │ │ S0/S1 │ │
│ │ └───────────┘ │
│ │ ┌───────────┐ │
│ │ │ 老年代 │ │
│ │ └───────────┘ │
├─────────────┴───────────────┤
│ 栈、PC寄存器、本地方法栈 │
└───────────────────────────────┘
永久代(PermGen) :这是JDK 7的"老大难"问题。它就像一个固定大小的仓库,用来存放类的元数据(类名、方法、常量等)。问题在于,这个仓库的大小是固定的,如果应用加载的类太多,就会出现OutOfMemoryError: PermGen space错误。
堆内存:分为新生代(Eden和Survivor区)和老年代。新生代是"新生儿",老年代是"老年人"。
1.2 JDK 7的GC算法:老式发动机
JDK 7主要使用以下GC算法:
- Serial GC:单线程回收,像一个人在打扫房间,简单但效率低
- Parallel GC:多线程回收,像多个人一起打扫,效率高但暂停时间长
- CMS GC:并发标记清除,像一边打扫一边继续工作,暂停时间短,但容易"内存碎片"
CMS GC的痛点:它在标记阶段会暂停所有应用线程(STW),虽然比其他GC暂停时间短,但仍然不够理想。而且,CMS在处理大堆内存时,容易出现"并发模式失败"。
2. JDK 21的JVM结构及GC算法:元空间的革命
2.1 JDK 21的JVM身体结构
JDK 21彻底改变了JVM的内存结构,最显著的变化是永久代(PermGen)被元空间(Metaspace)取代:
┌───────────────────────────────┐
│ JVM内存结构 │
├─────────────┬───────────────┤
│ 元空间(Metaspace) │ 堆内存 │
│ │ ┌───────────┐ │
│ │ │ 新生代 │ │
│ │ │ ├───────┤ │
│ │ │ │ Eden │ │
│ │ │ ├───────┤ │
│ │ │ │ S0/S1 │ │
│ │ └───────────┘ │
│ │ ┌───────────┐ │
│ │ │ 老年代 │ │
│ │ └───────────┘ │
├─────────────┴───────────────┤
│ 栈、PC寄存器、本地方法栈 │
└───────────────────────────────┘
元空间(Metaspace) :这是JDK 8引入的革命性变化,它使用本地内存(Native Memory)来存储类的元数据,不再受JVM堆内存的限制。就像把仓库从固定大小的房间搬到了可以无限扩展的仓库,再也不用担心"仓库满了"的问题。
堆内存:与JDK 7相比,堆内存结构基本保持不变,但GC算法有了质的飞跃。
2.2 JDK 21的GC算法:现代低延迟发动机
JDK 21中,GC算法有了重大升级:
- G1 GC:默认GC算法,JDK 9开始,是JDK 7中Parallel GC的升级版
- ZGC:低延迟GC,JDK 11开始,暂停时间通常在10ms以下
- Shenandoah GC:低延迟GC,JDK 12开始,与ZGC类似,但实现方式不同
ZGC的"黑科技":ZGC使用了"着色指针"(Colored Pointers)和"并发重映射"(Concurrent Remapping)技术,可以在应用线程运行的同时进行垃圾回收,实现几乎"零暂停"。
Shenandoah GC:它使用"Brooks Pointer"技术,通过提前准备指针,实现并发回收,暂停时间也控制在10ms以下。
3. JDK 7 vs JDK 21:JVM结构与GC算法对比
| 特性 | JDK 7 | JDK 21 | 说明 |
|---|---|---|---|
| 内存结构 | 永久代(PermGen) | 元空间(Metaspace) | 永久代被移除,元空间使用本地内存 |
| 默认GC | Parallel GC | G1 GC | JDK 21默认使用G1 GC |
| 低延迟GC | 无 | ZGC, Shenandoah GC | JDK 21提供真正的低延迟GC |
| 永久代问题 | 频繁出现PermGen space错误 |
不存在 | 元空间自动扩展,不再受限 |
| GC暂停时间 | CMS: 100-500ms | ZGC: <10ms | ZGC实现了真正的低延迟 |
| GC算法数量 | 3种 | 5种+ | JDK 21提供更多选择 |
| JDK 8+支持 | 无 | 有 | 永久代在JDK 8中开始被移除 |
关键变化总结:
- 永久代(PermGen)被元空间(Metaspace)取代,解决了"类加载器内存溢出"问题
- CMS GC在JDK 14中被移除,G1 GC成为默认GC
- ZGC和Shenandoah GC成为低延迟应用的首选
4. 现代版本推荐的GC算法:为什么选择它们?
4.1 G1 GC:平衡之王
为什么推荐:G1 GC在吞吐量和延迟之间取得了良好的平衡,特别适合大多数中大型应用。它的设计目标是"可预测的停顿时间",而不是"最大吞吐量"。
大白话解释:想象一下,你正在超市购物,G1 GC就像一个高效的收银员,它会提前计算好你需要排队的时间,让你知道大概要等多久,而不是让你在收银台前干等。
适用场景:
- 中大型Java应用
- 对延迟有一定要求,但不需要极致低延迟
- 堆内存在4GB到32GB之间的应用
4.2 ZGC:低延迟的王者
为什么推荐:ZGC的暂停时间可以控制在10ms以下,甚至可以达到1ms,这对于实时交易系统、高频交易、在线游戏等场景至关重要。
大白话解释:ZGC就像一个超级高效的快递员,他可以在你购物的同时,把包裹准确地送到你手中,几乎不会让你等待。
适用场景:
- 低延迟要求极高的应用(如高频交易系统)
- 实时系统(如在线游戏、实时通信)
- 堆内存较大的应用(>4GB)
4.3 Shenandoah GC:ZGC的替代品
为什么推荐:Shenandoah GC与ZGC类似,但实现方式不同。它在某些场景下可能比ZGC表现更好,特别是在多CPU系统上。
大白话解释:Shenandoah GC就像ZGC的"双胞胎兄弟",他们都能做到低延迟,但使用了不同的技术。
适用场景:
- 与ZGC类似,但可能在某些硬件配置上表现更好
- 需要与ZGC对比测试的场景
5. 源码解析:GC算法的执行流程
5.1 G1 GC的核心流程
G1 GC的执行流程可以简化为以下几个步骤:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 初始标记 │───▶│ 并发标记 │───▶│ 重新标记 │───▶│ 清理阶段 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
(STW) (并发) (STW) (并发)
关键点:
- 初始标记:暂停所有应用线程(STW),标记根对象
- 并发标记:应用线程继续运行,GC线程标记可达对象
- 重新标记:暂停所有应用线程(STW),修正并发标记期间的变化
- 清理阶段:应用线程继续运行,GC线程清理无用对象
5.2 ZGC的核心流程
ZGC的执行流程更加复杂,但核心思想是"并发":
┌─────────────────────────────────────────────────────────────────────┐
│ ZGC的"着色指针"与"并发重映射"技术 │
├─────────────────┬─────────────────┬─────────────────┬─────────────────┤
│ 1. 读取对象 │ 2. 重映射指针 │ 3. 重映射完成 │ 4. 清理垃圾 │
│ (应用线程) │ (GC线程) │ (GC线程) │ (GC线程) │
└─────────────────┴─────────────────┴─────────────────┴─────────────────┘
关键点:
- 着色指针:ZGC使用指针的某些位来标记对象状态(如"正在被GC")
- 并发重映射:GC线程在应用线程运行的同时,逐步重映射对象指针
- 几乎零停顿:应用线程在GC过程中几乎不会被暂停
5.3 源码解析:ZGC的着色指针实现
ZGC的着色指针实现是其核心创新之一,以下是简化版的代码逻辑:
java
// ZGC中着色指针的实现(简化版)
public class ZCObject {
// 指针的高4位用于着色
private long pointer;
// 着色状态
private enum Color {
UNCOLORED, // 未着色
MARKED, // 已标记
REMAPPED, // 已重映射
COLORED // 已着色
}
// 获取对象的着色状态
public Color getColor() {
// 提取指针的高4位
int colorBits = (int) ((pointer >> 60) & 0xF);
return Color.values()[colorBits];
}
// 设置对象的着色状态
public void setColor(Color color) {
// 清除高4位
long newPointer = pointer & 0x0FFFFFFFFFFFFFFF;
// 设置高4位
newPointer |= ((long) color.ordinal() << 60);
pointer = newPointer;
}
}
大白话解释:ZGC就像在每件物品上贴了一个小标签(着色指针),这个标签有4位,可以表示16种状态。当GC需要处理某个对象时,它会查看这个标签,决定如何处理。应用线程在处理物品时,会看到标签,知道这个物品是否正在被GC处理。
六、实战:GC算法选择与配置
6.1 选择GC算法的简单指南
java
// JDK 21中配置GC算法的示例
public class GcConfigExample {
public static void main(String[] args) {
// G1 GC(默认)
System.out.println("G1 GC (default): -XX:+UseG1GC");
// ZGC
System.out.println("ZGC: -XX:+UseZGC");
// Shenandoah GC
System.out.println("Shenandoah GC: -XX:+UseShenandoahGC");
// 设置最大暂停时间(ZGC示例)
System.out.println("ZGC with max pause time: -XX:MaxGCPauseMillis=10");
}
}
6.2 实际应用中的GC配置
java
// 生产环境推荐的GC配置示例
public class ProductionGcConfig {
public static void main(String[] args) {
// 对于大多数应用,G1 GC是好的选择
// 4GB堆内存,目标暂停时间100ms
System.out.println("G1 GC with 4GB heap, max pause 100ms:");
System.out.println("java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100");
// 对于低延迟应用,ZGC是更好的选择
System.out.println("ZGC with 8GB heap, max pause 10ms:");
System.out.println("java -Xms8g -Xmx8g -XX:+UseZGC -XX:MaxGCPauseMillis=10");
}
}
7. 结论:JVM的进化之路
从JDK 7到JDK 21,JVM的进化不仅仅是技术的升级,更是对Java应用性能和稳定性的重大提升。元空间的引入 解决了永久代的内存限制问题,ZGC和Shenandoah GC的出现则为低延迟应用提供了强大支持。
为什么这些变化重要?
- 永久代→元空间:避免了类加载器内存溢出,让应用更稳定
- CMS→G1:G1在吞吐量和延迟之间取得了更好的平衡
- G1→ZGC/Shenandoah:实现了真正的低延迟,满足了现代应用的需求
8. 经典书籍推荐
-
《Java Performance》 by Scott Oaks (最新版)
- 2024年最新版,详细讲解了JVM性能调优、GC算法和内存管理
- 书中包含大量实战案例,特别适合生产环境调优
- 书中有一章专门讲解ZGC和Shenandoah GC的实践
-
《The Garbage Collection Handbook》 by Richard Jones, Antony Hosking, Eliot Moss
- GC领域的"圣经",详细讲解了各种GC算法的原理和实现
- 虽然出版较早,但内容依然权威,是理解GC的必读之作
-
《Java Virtual Machine Specification》 (Java SE 21)
- Java官方文档,详细描述了JVM的规范和实现
- 适合深入研究JVM内部机制
9. 写在最后
JDK 7到JDK 21的JVM进化,就像从老式自行车升级到现代电动车。虽然自行车也能骑,但电动车更舒适、更高效、更可靠。同样,JDK 21的JVM结构和GC算法,为Java应用提供了更强大、更稳定的运行环境。
作为Java开发者,了解JVM的演进和GC算法,不仅有助于解决性能问题,更能让我们写出更高质量的代码。就像了解汽车的发动机原理,能让我们更好地驾驶和维护车辆一样。
最后提醒:不要盲目追求"最新"GC算法,而是根据应用需求选择最适合的。对于大多数应用,G1 GC已经足够;对于低延迟要求高的应用,ZGC或Shenandoah GC是更好的选择。
参考链接:
本文为原创,遵循 CC 4.0 BY-SA 协议。转载请附原文链接及本声明。