垃圾回收机制
垃圾回收(Garbage Collection,GC)是JVM自动内存管理的核心机制。它负责自动回收不再使用的对象所占用的内存,让开发者从繁琐的内存管理中解放出来。理解GC机制对于编写高性能Java应用和排查内存问题至关重要。
为什么需要垃圾回收?
手动内存管理的问题
在C/C++等语言中,开发者需要手动管理内存:
// C语言手动内存管理
void example() {
int* ptr = (int*)malloc(sizeof(int) * 100);
// 使用ptr...
free(ptr); // 必须手动释放,否则内存泄漏
// 忘记释放?内存泄漏!
// 重复释放?程序崩溃!
// 释放后继续使用?未定义行为!
}
Java的自动内存管理
Java通过垃圾回收机制自动管理内存:
public class GCExample {
public void example() {
Object obj = new Object();
// 使用obj...
// 无需手动释放,GC会自动回收
}
}
如何判断对象可以被回收?
引用计数法
引用计数法通过记录对象被引用的次数来判断是否可以回收:
对象A(引用计数=2)
├── 引用 → 对象B(引用计数=1)
└── 引用 → 对象C(引用计数=0) ← 可回收
引用计数法的工作原理:
- 每当有一个地方引用对象,计数+1
- 每当引用失效,计数-1
- 计数为0时,表示没有引用,可以回收
优点:实现简单,效率高
缺点:无法解决循环引用问题
// 循环引用示例
public class ReferenceCountingGC {
public Object instance = null;
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB; // A引用B
objB.instance = objA; // B引用A
objA = null;
objB = null;
// 循环引用,引用计数都不为0,无法回收!
// objA计数=1, objB计数=1
}
}
问题说明:尽管objA和objB都不再被外部引用,但由于它们相互引用,引用计数都是1,引用计数法无法识别出这是垃圾。这是引用计数法的致命缺陷。
可达性分析算法
JVM使用可达性分析算法来判断对象是否存活。从"GC Roots"出发,搜索所有可达的对象,不可达的对象即为垃圾。
GC Roots(垃圾回收根节点)
│
├── 栈中引用 → 对象1(存活)→ 对象2(存活)→ 对象3(存活)
│ ↘ 对象4(存活)
│
├── 方法区静态属性 → 对象1(存活)
│
├── 方法区常量引用 → ...
│
└── 本地方法栈JNI引用 → ...
对象5 → 对象6(循环引用,但不可从GC Roots到达,对象5和6可回收)
可达性分析算法的工作原理:
- 从GC Roots出发,通过引用链遍历所有对象
- 能够被GC Roots访问到的对象称为"可达对象"(存活)
- 无法被GC Roots访问到的对象称为"不可达对象"(可回收)
- 即使对象之间有循环引用,只要从GC Roots无法到达,同样会被回收
GC Roots包括:
- 虚拟机栈中引用的对象:方法中的局部变量、参数
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- 同步锁持有的对象
- JVM内部引用:基本类型的Class对象、常驻异常对象等
Java中的引用类型
JDK1.2之后,Java将引用分为四种类型,更灵活地管理对象生命周期:
1. 强引用(Strong Reference)
最常见的引用类型,只要强引用存在,垃圾回收器永远不会回收。
Object obj = new Object(); // 强引用
obj = null; // 解除引用,对象才可能被回收
2. 软引用(Soft Reference)
内存不足时会被回收,适合实现缓存。
import java.lang.ref.SoftReference;
public class SoftReferenceExample {
public static void main(String[] args) {
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<>(obj);
obj = null; // 解除强引用
// 内存不足时,softRef.get()可能返回null
System.out.println(softRef.get());
}
}
3. 弱引用(Weak Reference)
无论内存是否充足,GC时都会被回收。
import java.lang.ref.WeakReference;
public class WeakReferenceExample {
public static void main(String[] args) {
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
obj = null;
System.gc(); // GC后,weakRef.get()返回null
System.out.println(weakRef.get());
}
}
4. 虚引用(Phantom Reference)
无法通过虚引用获取对象,用于跟踪对象被垃圾回收的活动。
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceExample {
public static void main(String[] args) {
Object obj = new Object();
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);
obj = null;
System.out.println(phantomRef.get()); // 始终返回null
// 当对象被回收后,可以从queue中获取到PhantomReference
}
}
垃圾回收算法
标记-清除算法(Mark-Sweep)
最基础的收集算法,分为"标记"和"清除"两个阶段:
阶段一:标记
标记前: [存活对象] [存活对象] [垃圾对象] [存活对象] [垃圾对象]
↓标记
标记后: [存活对象] [存活对象] [X标记] [存活对象] [X标记]
阶段二:清除
清除后: [存活对象] [存活对象] [空闲空间] [存活对象] [空闲空间]
算法执行过程:
- 标记阶段:遍历所有对象,标记存活的对象(标记为"存活")
- 清除阶段:遍历堆中所有对象,回收未被标记的对象(垃圾)的内存空间
优点:实现简单
缺点:
- 效率问题:标记和清除效率都不高,需要遍历整个堆
- 空间问题:产生大量不连续的内存碎片,导致后续分配大对象时可能触发新的GC
复制算法(Copying)
将内存分为两块,每次只使用一块。当一块用完,将存活对象复制到另一块,然后清空当前块。
复制算法执行过程:
复制前:
┌─────────────────────┐ ┌─────────────────────┐
│ From区 │ │ To区 │
│ (正在使用) │ │ (空闲) │
│ │ │ │
│ [对象1] 存活 │ │ │
│ [垃圾] │ │ │
│ [对象2] 存活 │ │ │
│ [垃圾] │ │ │
│ [对象3] 存活 │ │ │
└─────────────────────┘ └─────────────────────┘
复制后:将存活对象复制到To区,然后清空From区
┌─────────────────────┐ ┌─────────────────────┐
│ From区 │ │ To区 │
│ (空闲) │ │ (正在使用) │
│ │ │ │
│ │ │ [对象1] [对象2] │
│ │ │ [对象3] │
└─────────────────────┘ └─────────────────────┘
优点:
- 没有碎片问题:复制后内存连续
- 实现简单,运行高效:只复制存活对象
缺点:
- 内存利用率低:只能使用一半的内存空间
- 复制开销与存活对象数量成正比:存活对象越多,复制成本越高
优化:HotSpot JVM将新生代分为Eden:Survivor:Survivor = 8:1:1的比例
新生代内存布局:
┌───────────────────────────────┬────────────┬────────────┐
│ Eden区 │ Survivor0 │ Survivor1 │
│ (80%) │ (10%) │ (10%) │
└───────────────────────────────┴────────────┴────────────┘
↑ 新对象分配位置 │ │
复制交换位置 │
↓ ↓
下次GC时的交换
每次Minor GC时,将Eden区和From区中存活的对象复制到To区,然后交换From区和To区的角色。
// 新生代内存布局
// |---- Eden ----|-- S0 --|-- S1 --|
// 80% 10% 10%
标记-整理算法(Mark-Compact)
标记后,将存活对象向一端移动,然后清理边界外的内存:
标记-整理算法执行过程:
标记后(标记为存活的对象):
[存活对象] [存活对象] [X垃圾] [存活对象] [X垃圾] [存活对象]
↓ 移动(整理)
整理后:
[存活对象] [存活对象] [存活对象] [空闲] [空闲] [空闲]
算法执行过程:
- 标记阶段:与标记-清除算法相同,标记所有存活对象
- 整理阶段:将所有存活对象向一端移动,紧凑排列
- 清除阶段:清理边界外的内存
优点:
- 没有碎片问题:存活对象连续排放
- 内存利用率高:没有内存碎片
缺点:
- 移动对象成本高:需要移动大量存活对象
- 需要更新引用:对象地址改变,需要更新所有引用
与标记-清除算法的对比:
| 特性 | 标记-清除 | 标记-整理 |
|---|---|---|
| 内存碎片 | 有 | 无 |
| 分配速度 | 慢(需找空闲位置) | 快(连续可用) |
| 执行速度 | 较快 | 较慢(需移动对象) |
| 适用场景 | 存活对象少 | 存活对象多 |
分代收集算法
根据对象存活周期的不同,将内存划分为几块,针对不同区域采用不同的收集算法。这是当前主流JVM采用的垃圾收集策略。
分代理论依据:
分代收集算法基于两个重要的假说,这两个假说在实际应用中得到了充分验证:
-
弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
- 统计表明,约80%-90%的新创建对象在很短时间内就会变成垃圾
- 新生代的对象死亡率极高,回收效率很高
- 这是新生代采用复制算法的理论基础
-
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集的对象越难消亡
- 存活时间越长的对象,继续存活的可能性越大
- 老年代的对象死亡率低,不适合频繁回收
- 这是减少老年代回收频率的理论基础
新生代(Young Generation)详解:
新生代是对象分配的主要区域,采用复制算法进行回收:
- Eden区(占新生代80%):新创建的对象首先在这里分配
- Survivor区(两个,各占10%):存放Minor GC后的存活对象
新生代内存分配与回收过程:
Survivor区角色不断交换,这就是"复制"的含义
老年代(Old Generation)详解:
老年代存放长期存活的对象,采用标记-整理或标记-清除算法:
对象进入老年代的条件:
- 年龄达到阈值:对象在Survivor区每熬过一次Minor GC,年龄就增加1,当年龄达到阈值(默认15)时晋升到老年代
// 可通过参数调整年龄阈值
-XX:MaxTenuringThreshold=15 // 默认值
- 大对象直接进入老年代:超过某个阈值的大对象直接在老年代分配
// 设置大对象阈值(单位字节)
-XX:PretenureSizeThreshold=3145728 // 3MB
-
动态年龄判定:如果Survivor区中相同年龄所有对象大小的总和大于Survivor区的一半,年龄大于或等于该年龄的对象直接进入老年代
-
空间担保失败:Minor GC前,如果老年代最大可用连续空间小于新生代所有对象总大小,且不允许担保失败,则直接Full GC
分代收集的优势:
- 效率高:新生代回收频率高但速度快,老年代回收频率低
- 内存利用率高:新生代采用8:1:1的比例,浪费空间少
- 停顿时间可控:Minor GC停顿时间短,Full GC虽然停顿长但频率低
- 适应性强:不同区域采用最适合的算法
垃圾收集器
JVM提供了多种垃圾收集器,各有特点:
Serial收集器
单线程收集器,进行垃圾回收时必须暂停所有工作线程。
工作原理:
执行过程:
- STW(Stop-The-World):暂停所有用户线程
- 单线程GC:使用单个GC线程进行垃圾回收(复制/标记-整理)
- 恢复执行:GC完成后,恢复用户线程运行
特点:
- 简单高效:没有线程切换开销,单线程效率高
- 客户端模式:适合在客户端模式下运行
- 小内存应用:堆内存较小的情况下表现良好
- 参数:
-XX:+UseSerialGC
使用场景:
- 客户端应用(如桌面GUI应用)
- 堆内存较小的应用(小于100MB)
- 单核处理器环境
新生代Serial收集器:使用复制算法 老年代Serial收集器:使用标记-整理算法
Parallel Scavenge收集器
多线程收集器,关注吞吐量(运行用户代码时间/总时间)。
特点:
- 吞吐量优先
- 适合后台计算任务
- 参数:
-XX:+UseParallelGC
相关参数:
-XX:MaxGCPauseMillis:最大垃圾收集停顿时间-XX:GCTimeRatio:吞吐量大小(0-100)-XX:+UseAdaptiveSizePolicy:自适应调节策略
CMS收集器(Concurrent Mark Sweep)
以获取最短回收停顿时间为目标的收集器。
四个阶段:
各阶段详细说明:
-
初始标记(Initial Mark):[STW暂停]
- 标记GC Roots直接关联的对象
- 速度很快,但需要暂停所有用户线程
- 停顿时间:通常很短(几十毫秒)
-
并发标记(Concurrent Mark)
- 从GC Roots出发,遍历整个对象图
- 与用户线程并发执行,不需要暂停
- 耗时较长,但不影响用户响应
-
重新标记(Remark):[STW暂停]
- 修正并发标记期间因用户线程运行产生的变化
- 停顿时间比初始标记长,但比串行收集器短
-
并发清除(Concurrent Sweep)
- 并发清除未被标记的垃圾对象
- 与用户线程并发执行
特点:
- 低停顿:大部分时间与用户线程并发执行
- 并发收集:GC线程和用户线程同时运行
- 参数:
-XX:+UseConcMarkSweepGC
缺点:
- 对CPU资源敏感:会占用CPU资源,影响用户线程
- 无法处理浮动垃圾:并发标记期间产生的垃圾称为"浮动垃圾",只能等下次GC
- 产生内存碎片:使用标记-清除算法,会产生内存碎片
CMS配置示例:
# 启用CMS收集器
-XX:+UseConcMarkSweepGC
# 设置GC停顿时间目标(毫秒)
-XX:MaxGCPauseMillis=100
# 触发CMS老年代占比阈值
-XX:CMSInitiatingOccupancyFraction=70
G1收集器(Garbage First)
面向服务端的垃圾收集器,将堆内存划分为多个大小相等的Region:
G1堆内存布局:
Region说明:
- E (Eden):新创建的对象分配区域
- S (Survivor):Minor GC后存活对象的区域
- O (Old):长期存活的对象区域
- H (Humongous):大对象区域(超过Region一半大小的对象)
G1的特点:
- 可预测停顿时间:可以设置期望的停顿时间目标,G1会尝试在目标时间内完成回收
- 无内存碎片:使用复制算法进行压缩,避免内存碎片
- 并行与并发:GC线程并行执行,部分阶段与用户线程并发
- 分代收集:仍然是分代收集器,区分年轻代和老年代
- 增量回收:不需要一次性回收整个老年代,而是分批回收
- 参数:
-XX:+UseG1GC(JDK 9+默认)
G1的核心设计理念:
G1的设计目标是"Garbage First"——优先回收垃圾最多的区域。G1通过追踪每个Region中垃圾的多少,优先选择回收价值最高的区域,从而在有限的时间内最大化回收效率。
工作模式:
- Young GC(年轻代收集):Eden区满时触发,只回收年轻代Region
- Concurrent Start:开始并发标记周期,为Mixed GC做准备
- Mixed GC:回收年轻代和部分老年代Region
- Full GC:内存不足时的兜底(应尽量避免)
G1收集周期详解:
G1收集周期:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Young-Only 阶段 │
│ │
│ [Normal YGC] → [Normal YGC] → [Concurrent Start] → [Remark] → [Cleanup] │
│ ↓ ↓ ↓ ↓ ↓ │
│ 普通年轻代 普通年轻代 并发标记开始 完成标记 清理 │
│ 收集 收集 +年轻代收集 +判断是否 │
│ 进入空间回收 │
│ ↓ │
│ 老年代占用达到IHOP阈值 │
│ ↓ │
├─────────────────────────────────────────────────────────────────────────────┤
│ Space-Reclamation 阶段 │
│ │
│ [Prepare Mixed] → [Mixed GC] → [Mixed GC] → ... → 回到Young-Only阶段 │
│ ↓ ↓ ↓ │
│ 准备混合回收 混合回收 混合回收 │
│ (年轻代+部分老年代) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
关键阈值:
- IHOP (Initiating Heap Occupancy Percent):触发并发标记的老年代占用阈值
默认45%,可通过-XX:InitiatingHeapOccupancyPercent调整
G1重要参数详解:
# 基本配置
-XX:+UseG1GC # 启用G1(JDK9+默认)
-XX:MaxGCPauseMillis=200 # 最大停顿时间目标(毫秒)
# Region大小配置
-XX:G1HeapRegionSize=4m # Region大小(1-32MB,2的幂)
# 年轻代配置
-XX:G1NewSizePercent=5 # 年轻代最小占比(默认5%)
-XX:G1MaxNewSizePercent=60 # 年轻代最大占比(默认60%)
# 并发标记配置
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用阈值
-XX:G1HeapReservePercent=10 # 保留空间百分比
# Mixed GC配置
-XX:G1MixedGCCountTarget=8 # Mixed GC次数目标
-XX:G1MixedGCLiveThresholdPercent=85 # Region存活对象超过此比例不回收
-XX:G1HeapWastePercent=5 # 允许的浪费空间百分比
# 大对象配置
-XX:G1HeapRegionSize # Region大小影响大对象阈值
# 大对象 = 超过Region一半的对象,直接分配到老年代
# 字符串去重(减少内存占用)
-XX:+UseStringDeduplication # 启用字符串去重
# 周期性GC(及时归还内存)
-XX:G1PeriodicGCInterval=0 # 周期性GC间隔(毫秒),0表示禁用
G1调优实战案例:
案例1:电商服务响应延迟优化
# 问题:P99响应时间不稳定,偶尔出现长停顿
# 分析:G1的停顿时间目标设置过大,导致每次回收更多Region
# 优化前
-XX:+UseG1GC -Xmx8g -Xms8g
# 优化后:降低停顿时间目标,提前触发并发标记
-XX:+UseG1GC -Xmx8g -Xms8g \
-XX:MaxGCPauseMillis=100 \
-XX:InitiatingHeapOccupancyPercent=35
案例2:大数据处理吞吐量优化
# 问题:批处理任务吞吐量不足,GC开销大
# 分析:年轻代太小,对象过早晋升到老年代
# 优化前
-XX:+UseG1GC -Xmx16g
# 优化后:增大年轻代,减少晋升
-XX:+UseG1GC -Xmx16g -Xms16g \
-XX:G1NewSizePercent=20 \
-XX:G1MaxNewSizePercent=40
G1与大对象:
G1对大对象(Humongous Objects)有特殊处理:
// 大对象定义:大小 >= Region大小 / 2
// 例如:Region大小为4MB,则大于等于2MB的对象为大对象
// 大对象特点:
// 1. 直接分配在老年代
// 2. 占用连续的Region
// 3. 只在并发标记结束或Full GC时回收
// 4. 可能导致内存碎片
// 建议:避免创建过多大对象
// 如果需要大数组,考虑拆分为小数组
G1与ZGC选择建议:
| 场景 | 推荐收集器 | 理由 |
|---|---|---|
| 通用服务端应用 | G1 | 平衡吞吐和延迟,成熟稳定 |
| 大内存(>32GB) | ZGC(分代) | 停顿时间不受堆大小影响 |
| 超低延迟要求 | ZGC(分代) | 停顿时间小于1ms |
| 批处理任务 | G1或Parallel | 吞吐量优先 |
| 容器环境(小内存) | G1 | 资源效率高 |
ZGC收集器
ZGC(Z Garbage Collector)是JDK 11引入的可扩展低延迟垃圾收集器。ZGC将所有耗时的工作并发执行,停顿时间不超过1毫秒,且停顿时间与堆大小无关。
核心特点:
- 超低延迟:停顿时间通常在1毫秒以内,远超G1的200毫秒目标
- 堆大小无关:停顿时间不随堆大小增长,支持从几百MB到16TB的堆
- 并发处理:标记、转移、引用处理等均并发执行
- 自适应调优:动态调整代大小、GC线程数、晋升阈值
分代ZGC(Generational ZGC)
JDK 21 引入了 Generational ZGC(分代ZGC),这是ZGC的重大升级。Oracle官方强烈建议用户迁移到分代ZGC。
为什么需要分代ZGC?
分代假说指出:
- 弱分代假说:大多数对象都是"朝生夕灭"的,存活时间很短
- 强分代假说:熬过多次GC的对象更难消亡
非分代ZGC每次回收都需要扫描整个堆,而分代ZGC利用分代假说,将堆划分为年轻代和老年代,频繁回收年轻代(对象死亡率高),偶尔回收老年代(对象死亡率低),显著提升性能。
分代ZGC vs 非分代ZGC 回收效率对比:
非分代ZGC:
┌────────────────────────────────────────────────────────┐
│ 整个堆 │
│ [年轻对象] [年轻对象] [老对象] [年轻对象] [老对象] │
│ ↓ │
│ 每次GC扫描整个堆(成本高) │
└────────────────────────────────────────────────────────┘
分代ZGC:
┌────────────────────┬───────────────────────────┐
│ 年轻代 │ 老年代 │
│ [年轻对象] [年轻对象] │ [老对象] [老对象] │
│ ↓ │ ↓ │
│ 频繁回收 │ 偶尔回收 │
│ (成本低) │ (成本低) │
└────────────────────┴───────────────────────────┘
分代ZGC的优势:
- 更低的CPU开销:年轻代回收频率高但成本低
- 更高的吞吐量:减少了全堆扫描的频率
- 更稳定的延迟:回收时间更可预测
启用方式:
# JDK 21+:启用分代ZGC(强烈推荐)
-XX:+UseZGC -XX:+ZGenerational
# JDK 11-20:使用非分代ZGC
-XX:+UseZGC
ZGC重要参数详解
| 参数 | 说明 | 示例 |
|---|---|---|
-XX:+UseZGC | 启用ZGC | -XX:+UseZGC |
-XX:+ZGenerational | 启用分代ZGC(JDK 21+,推荐) | -XX:+ZGenerational |
-XX:SoftMaxHeapSize | 软最大堆大小,ZGC会尽量不超过此值,但允许在必要时扩展到-Xmx | -XX:SoftMaxHeapSize=4g |
-XX:-ZUncommit | 禁用内存归还给操作系统 | -XX:-ZUncommit |
-XX:ZUncommitDelay | 内存归还延迟(秒),默认300秒 | -XX:ZUncommitDelay=300 |
-XX:+UseLargePages | 启用大页(提升性能) | -XX:+UseLargePages |
-XX:+AlwaysPreTouch | 启动时预分配内存(减少运行时开销) | -XX:+AlwaysPreTouch |
-XX:ZAllocationSpikeTolerance | 分配峰值容忍度,默认2 | -XX:ZAllocationSpikeTolerance=2 |
-XX:ZFragmentationLimit | 最大碎片率百分比,默认5 | -XX:ZFragmentationLimit=5 |
软最大堆大小(SoftMaxHeapSize)详解:
软最大堆大小是ZGC的一个重要概念。它允许设置一个"软限制",ZGC会努力将堆保持在这个大小以下,但在必要时可以扩展到-Xmx指定的最大值。
# 示例:软限制4GB,硬限制5GB
java -Xmx5g -XX:SoftMaxHeapSize=4g -XX:+UseZGC -XX:+ZGenerational -jar app.jar
这种配置的优势:
- 正常情况下保持4GB内存占用
- 在分配峰值时可以临时扩展到5GB
- 避免因内存不足导致应用停顿
内存归还(Uncommit)机制:
ZGC默认会将未使用的内存归还给操作系统,这在容器环境或内存受限场景很有用。但对延迟敏感的应用,可以考虑禁用:
# 方式1:禁用内存归还(推荐用于低延迟场景)
-XX:-ZUncommit
# 方式2:设置-Xms等于-Xmx(隐式禁用)
-Xms16g -Xmx16g -XX:+AlwaysPreTouch
大页配置(Linux):
使用大页可以显著提升ZGC性能。大页减少了TLB(Translation Lookaside Buffer)缺失,提高内存访问效率。
# 配置大页(假设16GB堆,需要8192个2MB页,预留2GB给JVM其他结构)
echo 9216 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 验证配置
cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
# 启用大页
java -XX:+UseZGC -XX:+ZGenerational -XX:+UseLargePages -Xmx16g -jar app.jar
# 或使用透明大页(需要内核>=4.7)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo advise > /sys/kernel/mm/transparent_hugepage/shmem_enabled
java -XX:+UseZGC -XX:+ZGenerational -XX:+UseLargePages -XX:+UseTransparentHugePages -jar app.jar
性能数据(来自Oracle官方测试):
根据Oracle内部的性能测试数据,分代ZGC相比非分代ZGC有显著提升:
| 指标 | 相比JDK 17非分代ZGC | 相比JDK 21非分代ZGC |
|---|---|---|
| 吞吐量 | 提升约10% | 提升约10%以上 |
| 平均延迟 | 略有增加(2-3微秒) | 略有增加(2-3微秒) |
| P99停顿时间 | 改善10-20%(20-30微秒) | 改善10-20% |
分配停顿问题解决:
分代ZGC的最大优势在于解决了非分代ZGC的"分配停顿"问题。当新对象分配速度超过ZGC回收内存的速度时,非分代ZGC会出现性能急剧下降。分代ZGC通过频繁扫描年轻代,有效避免了这个问题。
以Apache Cassandra的测试为例:
- 在75个并发客户端以下,两种ZGC性能相近
- 超过75个并发客户端时,非分代ZGC遇到分配停顿,性能急剧下降
- 分代ZGC即使达到275个并发客户端,仍保持稳定的停顿时间
适用场景:
- 需要超低延迟的应用(如金融交易系统、实时游戏服务器)
- 大内存应用(从几百MB到16TB)
- 对停顿时间敏感的实时系统
- 高并发、高分配率的应用
- 云原生环境中的大内存服务
最佳实践配置示例:
# 生产环境推荐配置(JDK 21+)
java -Xmx16g -Xms16g \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:+UseLargePages \
-XX:+AlwaysPreTouch \
-Xlog:gc*:file=/var/log/gc.log:time,level,tags \
-jar app.jar
# 延迟优先配置(禁用内存归还)
java -Xmx16g -Xms16g \
-XX:+UseZGC \
-XX:+ZGenerational \
-XX:-ZUncommit \
-XX:+AlwaysPreTouch \
-jar app.jar
# 容器环境配置
java -XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseZGC \
-XX:+ZGenerational \
-jar app.jar
使用JDK Flight Recorder分析ZGC:
# 启动时启用JFR
java -XX:+UseZGC -XX:+ZGenerational \
-XX:StartFlightRecording=filename=zgc.jfr,settings=profile \
-jar app.jar
# 使用JDK Mission Control分析zgc.jfr文件
# 可以查看GC概览、配置、摘要等信息
Shenandoah收集器
Shenandoah是OpenJDK的低延迟垃圾收集器,由Red Hat开发并贡献给OpenJDK社区。与ZGC目标相似,但在实现机制上有所不同。
核心特点:
- 低停顿:停顿时间通常在0-10毫秒
- 并发压缩:在应用运行时进行对象移动和压缩,这是通过Brooks指针实现的
- 堆大小无关:停顿时间不随堆大小增长,200GB和2GB堆的停顿时间相近
Brooks指针技术:
Shenandoah使用Brooks指针(转发指针)来实现并发对象移动:
对象内存布局(Brooks指针):
┌──────────────────────────────────────────────────┐
│ 转发指针 │ 对象头 │ 实例数据 │ 填充 │
│ (8字节) │ │ │ │
└──────────────────────────────────────────────────┘
↓
指向对象本身或新位置
工作原理:
1. 每个对象前都有一个转发指针
2. 对象移动时,转发指针指向新位置
3. 读屏障检查转发指针,确保访问正确的对象位置
4. 这使得对象可以在应用运行时被移动
工作阶段:
Shenandoah GC 周期:
1. Pause Init Mark(初始标记停顿)~0.77ms
└── 标记GC Roots直接关联的对象
2. Concurrent marking(并发标记)
└── 遍历对象图,与应用并发执行
3. Pause Final Mark(最终标记停顿)~1.8ms
└── 完成标记,计算回收区域
4. Concurrent cleanup(并发清理)
└── 回收立即可以释放的区域
5. Concurrent evacuation(并发转移)
└── 将存活对象复制到新区域,更新转发指针
6. Pause Init Update Refs(更新引用停顿)~0.08ms
└── 准备更新引用阶段
7. Concurrent update references(并发更新引用)
└── 更新所有指向移动对象的引用
8. Pause Final Update Refs(最终更新引用停顿)~0.4ms
└── 完成引用更新
9. Concurrent cleanup(并发清理)
└── 回收旧区域
启用方式:
-XX:+UseShenandoahGC
重要参数详解:
| 参数 | 说明 | 示例 |
|---|---|---|
-XX:ShenandoahGCMode | GC模式 | normal(默认)/satb/passive |
-XX:ShenandoahGCHeuristics | 启发式策略 | adaptive(默认)/compact/static/aggressive |
-XX:ConcGCThreads | 并发GC线程数 | 默认为CPU核心数的1/4 |
-XX:ShenandoahMinRegionSize | 最小Region大小 | 默认256KB |
-XX:ShenandoahMaxRegionSize | 最大Region大小 | 默认32MB |
-XX:ShenandoahGarbageThreshold | 触发GC的垃圾占比 | 默认60% |
启发式策略说明:
# 自适应模式(默认)- 根据运行时情况自动调整
-XX:ShenandoahGCHeuristics=adaptive
# 紧凑模式 - 更积极的回收,适合内存受限环境
-XX:ShenandoahGCHeuristics=compact
# 静态模式 - 固定阈值,适合可预测的工作负载
-XX:ShenandoahGCHeuristics=static
# 激进模式 - 用于测试和诊断
-XX:ShenandoahGCHeuristics=aggressive
ZGC vs Shenandoah 对比:
| 特性 | ZGC(分代) | Shenandoah |
|---|---|---|
| 停顿时间目标 | 小于1ms | 小于10ms |
| 最大堆支持 | 16TB | 理论无限制 |
| 并发压缩 | 是(染色指针) | 是(Brooks指针) |
| 分代支持 | JDK 21+(推荐分代) | 不支持分代 |
| 内存开销 | 约15-20% | 约10-15% |
| 实现来源 | Oracle | Red Hat |
| JDK版本支持 | JDK 11+ | JDK 12+ |
适用场景:
- 需要可预测的低停顿时间(比ZGC稍宽松)
- 大内存应用
- OpenJDK发行版(如Adoptium、Amazon Corretto)
- 对吞吐量要求不是最高优先级
配置示例:
# 生产环境推荐配置
java -Xmx8g -Xms8g \
-XX:+UseShenandoahGC \
-XX:ShenandoahGCHeuristics=adaptive \
-XX:ConcGCThreads=2 \
-Xlog:gc*:file=/var/log/gc.log:time,level,tags \
-jar app.jar
# 内存受限环境
java -Xmx4g -Xms4g \
-XX:+UseShenandoahGC \
-XX:ShenandoahGCHeuristics=compact \
-jar app.jar
垃圾收集器选择
根据Oracle官方文档(JDK 21 GC调优指南),选择垃圾收集器应遵循以下原则:
官方推荐的选择流程
第一步:让JVM自动选择
除非应用有严格的停顿时间要求,否则首先让JVM自动选择收集器。JDK 9及以后版本默认使用G1收集器,对于大多数应用来说已经足够。
第二步:调整堆大小
如果性能不满足需求,首先尝试调整堆大小。很多时候,增大堆内存就能解决问题,无需更换收集器。
# 简单的堆大小调整
-Xmx4g -Xms4g
第三步:根据场景选择收集器
根据应用特点和需求选择合适的收集器:
详细选择指南
按应用类型选择
| 应用类型 | 推荐收集器 | 参数配置 |
|---|---|---|
| 小数据集(小于100MB) | Serial | -XX:+UseSerialGC |
| 单处理器环境 | Serial | -XX:+UseSerialGC |
| 批处理任务 | Parallel | -XX:+UseParallelGC |
| 数据分析应用 | Parallel | -XX:+UseParallelGC |
| Web服务 | G1(默认) | 无需指定 |
| 微服务 | G1(默认) | 无需指定 |
| 金融交易系统 | ZGC(分代) | -XX:+UseZGC -XX:+ZGenerational |
| 实时游戏服务器 | ZGC(分代) | -XX:+UseZGC -XX:+ZGenerational |
| 大内存缓存 | ZGC(分代) | -XX:+UseZGC -XX:+ZGenerational |
按性能指标选择
| 优先指标 | 推荐收集器 | 说明 |
|---|---|---|
| 吞吐量优先 | Parallel | GC时间占比最小化,适合批处理 |
| 延迟优先(一般) | G1 | 停顿时间可控,适合大多数服务端应用 |
| 延迟优先(严格) | ZGC(分代) | 停顿时间小于1ms,适合实时系统 |
| 内存占用最小 | Serial | 单线程,内存开销最小 |
按堆大小选择
| 堆大小 | 推荐收集器 | 理由 |
|---|---|---|
| 小于512MB | Serial | 小内存下简单高效 |
| 512MB - 4GB | G1 | 平衡吞吐量和延迟 |
| 4GB - 32GB | G1 | G1的最佳工作区间 |
| 大于32GB | ZGC(分代) | 大内存下停顿时间不受影响 |
收集器对比总结
| 收集器 | 停顿时间 | 吞吐量 | 堆大小支持 | JDK版本 | 适用场景 |
|---|---|---|---|---|---|
| Serial | 较长(秒级) | 中等 | 小(小于512MB) | 所有 | 客户端、小内存 |
| Parallel | 长(秒级) | 高 | 中-大 | 所有 | 批处理、吞吐优先 |
| G1 | 可控(小于200ms) | 高 | 中-大(4-32GB) | JDK 7+ | 服务端通用(默认) |
| ZGC(分代) | 极短(小于1ms) | 中-高 | 极大(16TB) | JDK 21+ | 低延迟、大内存 |
| ZGC(非分代) | 极短(小于1ms) | 中等 | 极大(16TB) | JDK 11+ | 低延迟、大内存 |
| Shenandoah | 短(小于10ms) | 中等 | 大 | JDK 12+ | 低延迟(OpenJDK) |
实际选择案例
案例1:电商平台后端服务
# 特点:请求量大,要求响应时间稳定
# 选择:G1(默认)
java -Xmx8g -Xms8g -XX:MaxGCPauseMillis=100 -jar app.jar
案例2:大数据分析任务
# 特点:批处理,吞吐量优先
# 选择:Parallel
java -Xmx32g -Xms32g -XX:+UseParallelGC -XX:GCTimeRatio=99 -jar app.jar
案例3:高频交易系统
# 特点:延迟敏感,要求微秒级响应
# 选择:分代ZGC
java -Xmx64g -Xms64g \
-XX:+UseZGC -XX:+ZGenerational \
-XX:+UseLargePages \
-XX:+AlwaysPreTouch \
-jar app.jar
案例4:容器化微服务(K8s)
# 特点:资源受限,需要自动适配
# 选择:G1(默认)+ 容器感知
java -XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=75.0 \
-XX:MaxGCPauseMillis=100 \
-jar app.jar
案例5:开发测试环境
# 特点:资源有限,快速启动
# 选择:Serial
java -Xmx512m -XX:+UseSerialGC -jar app.jar
收集器迁移指南
从CMS迁移(JDK 14已移除CMS):
# CMS配置(旧)
-XX:+UseConcMarkSweepGC
# 迁移到G1(推荐)
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# 或迁移到ZGC(如果需要更低延迟)
-XX:+UseZGC -XX:+ZGenerational
从Parallel迁移到G1:
# Parallel配置(旧)
-XX:+UseParallelGC
# 迁移到G1
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# 注意:吞吐量可能略有下降,但延迟会改善
从非分代ZGC迁移到分代ZGC:
# 非分代ZGC(JDK 11-20)
-XX:+UseZGC
# 分代ZGC(JDK 21+)
-XX:+UseZGC -XX:+ZGenerational
# 注意:吞吐量会提升,延迟保持不变或更优
内存分配与回收策略
理解JVM的内存分配策略对于编写高性能Java程序至关重要。本节详细介绍对象如何在堆中分配,以及JVM如何决定对象的回收时机。
对象优先在Eden分配
大多数情况下,新创建的对象会在新生代的Eden区分配。当Eden区没有足够空间进行分配时,JVM会触发一次Minor GC。
/**
* 演示对象在Eden区分配
* JVM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8
*/
public class EdenAllocation {
public static void main(String[] args) {
// 新生代总大小10MB,Eden占8MB,两个Survivor各占1MB
byte[] allocation1 = new byte[2 * 1024 * 1024]; // 2MB
byte[] allocation2 = new byte[2 * 1024 * 1024]; // 2MB
byte[] allocation3 = new byte[2 * 1024 * 1024]; // 2MB
// 此时Eden区已用6MB,还有约2MB空间
// 再分配4MB对象时,Eden区空间不足
byte[] allocation4 = new byte[4 * 1024 * 1024]; // 触发Minor GC
// GC后发现Survivor区放不下,通过空间担保机制进入老年代
}
}
执行过程解析:
初始状态:
┌─────────────────────────────────────────────────────────┐
│ Eden (8MB) │ S0 (1MB) │ S1 (1MB) │ 老年代 (10MB) │
│ [空] │ [空] │ [空] │ [空] │
└─────────────────────────────────────────────────────────┘
分配allocation1、2、3后:
┌─────────────────────────────────────────────────────────┐
│ Eden (8MB) │ S0 (1MB) │ S1 (1MB) │ 老年代 (10MB) │
│ [2MB][2MB][2MB]│ [空] │ [空] │ [空] │
│ 已用6MB │ │ │ │
└─────────────────────────────────────────────────────────┘
分配allocation4时触发Minor GC:
┌─────────────────────────────────────────────────────────┐
│ Eden (8MB) │ S0 (1MB) │ S1 (1MB) │ 老年代 (10MB) │
│ [allocation4] │ [空] │ [空] │ [allocation1,2,3]│
│ 4MB │ │ │ 6MB(空间担保) │
└─────────────────────────────────────────────────────────┘
大对象直接进入老年代
大对象是指需要大量连续内存空间的Java对象,典型的大对象有:
- 长字符串
- 大数组
- 大型数据结构
大对象直接进入老年代可以避免在Eden区和Survivor区之间发生大量的内存复制。
/**
* 演示大对象直接进入老年代
* JVM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
* -XX:PretenureSizeThreshold=3145728 (3MB)
* -XX:SurvivorRatio=8
*/
public class BigObjectAllocation {
public static void main(String[] args) {
// 4MB的大对象直接进入老年代
byte[] largeObject = new byte[4 * 1024 * 1024];
// 注意:PretenureSizeThreshold参数只对Serial和ParNew收集器有效
// Parallel Scavenge收集器不认识这个参数
}
}
为什么大对象要直接进入老年代?
如果大对象在Eden区分配:
Eden区 Survivor区
┌──────────────────────────┐ ┌──────────────┐
│ [大对象 4MB][其他对象] │ │ [空] │
└──────────────────────────┘ └──────────────┘
│
│ Minor GC时需要复制
↓
┌──────────────────────────┐ ┌──────────────┐
│ [其他存活对象] │ ←──│ [大对象 4MB] │
└──────────────────────────┘ │ 需要4MB空间 │
└──────────────┘
↓
Survivor区可能
放不下!
所以大对象直接进入老年代可以避免这个问题
长期存活对象进入老年代
JVM为每个对象定义了一个对象年龄(Age)计数器。对象在Survivor区每熬过一次Minor GC,年龄就增加1岁。当年龄增加到一定程度(默认15),就会被晋升到老年代。
/**
* 演示长期存活对象进入老年代
* JVM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
* -XX:MaxTenuringThreshold=1 -XX:SurvivorRatio=8
*/
public class TenuringThreshold {
public static void main(String[] args) {
byte[] allocation1 = new byte[1024 * 1024]; // 1MB
// 触发第一次Minor GC
System.gc(); // 仅作演示,实际不要在代码中调用
// allocation1年龄变为1,由于阈值设为1,会晋升到老年代
}
}
年龄计数器的工作原理:
对象头中的年龄信息:
┌────────────────────────────────────────────────────────────┐
│ 对象头 (Object Header) │
├────────────────────────────────────────────────────────────┤
│ Mark Word (32位/64位) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ hash:25 │ age:4 │ biased_lock:1 │ lock:2 │ ... │ │
│ │ │ ↑ │ │ │ │ │
│ │ │ 年龄 │ │ │ │ │
│ └─────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
age字段只有4位,最大值为15,这就是为什么MaxTenuringThreshold最大只能设为15
动态对象年龄判定
JVM并不是永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代。如果Survivor区中相同年龄所有对象大小的总和大于Survivor区的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。
/**
* 演示动态年龄判定
* JVM参数:-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
* -XX:MaxTenuringThreshold=15 -XX:SurvivorRatio=8
* -XX:+PrintTenuringDistribution
*/
public class DynamicAge {
public static void main(String[] args) {
// Survivor区大小为1MB
// 假设Eden区已有多个年龄为1的对象,总大小超过512KB(Survivor区一半)
// 那么这些对象会直接晋升到老年代
byte[] allocation1 = new byte[512 * 1024 / 2]; // 256KB
byte[] allocation2 = new byte[512 * 1024 / 2]; // 256KB
// 触发GC后,allocation1和allocation2年龄变为1
// 总大小512KB = Survivor区一半,会晋升到老年代
}
}
空间担保机制
在Minor GC之前,JVM会检查老年代最大可用的连续空间是否大于新生代所有对象的总大小:
空间担保检查流程:
┌─────────────────────────────┐
│ Minor GC 即将发生 │
└─────────────────────────────┘
│
↓
┌───────────────────────────────────────┐
│ 老年代最大可用空间 > 新生代所有对象大小?│
└───────────────────────────────────────┘
│ │
是 否
↓ ↓
┌──────────────────┐ ┌───────────────────────────┐
│ 安全进行Minor GC │ │ 检查HandlePromotionFailure │
└──────────────────┘ └───────────────────────────┘
│
┌─────────────┴─────────────┐
允许担保失败 不允许
↓ ↓
┌─────────────────────────────┐ ┌──────────────┐
│老年代空间 > 历次晋升平均大小?│ │ Full GC │
└─────────────────────────────┘ └──────────────┘
│ │
是 否
↓ ↓
┌──────────────┐ ┌──────────────┐
│ Minor GC │ │ Full GC │
└──────────────┘ └──────────────┘
为什么需要空间担保?
新生代采用复制算法,Minor GC时存活对象需要复制到Survivor区或老年代。如果Survivor区不够,就需要老年代进行"担保",接收这些对象。如果老年代也没有足够空间,就会导致担保失败,触发Full GC。
/**
* 空间担保相关参数
*/
// JDK 6后不再需要手动设置,JVM会自动处理
// -XX:-HandlePromotionFailure // 不允许担保失败(已废弃)
对象分配流程总结
GC日志分析
开启GC日志
# JDK8及之前
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# JDK9及之后
-Xlog:gc*:file=gc.log:time,level,tags
日志示例分析
[GC (Allocation Failure) [PSYoungGen: 6144K->808K(7168K)] 6144K->4888K(25600K), 0.0034567 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
解读:
GC:垃圾收集类型(GC或Full GC)Allocation Failure:触发原因PSYoungGen:收集器名称和区域6144K->808K:收集前后该区域使用量(7168K):该区域总大小6144K->4888K(25600K):堆内存使用情况0.0034567 secs:收集耗时
GC问题排查实战
排查工具与方法论
系统化排查流程:
案例1:频繁Full GC导致服务不可用
现象描述: 电商服务在促销期间频繁Full GC,每次停顿3-5秒,导致请求超时。
排查过程:
# 1. 查看GC统计
jstat -gcutil <pid> 1000 5
# 输出示例:
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 100.00 95.00 98.00 95.00 90.00 150 12.500 25 75.000 87.500
# ↑
# 老年代使用率98%,FGC次数25次
# 2. 生成堆转储
jmap -dump:live,format=b,file=heap.hprof <pid>
# 3. 使用MAT分析,发现某个缓存集合持续增长
问题根因:
// 问题代码:缓存没有过期机制
public class OrderCache {
// 所有订单都存入缓存,从不清理
private static final Map<String, Order> cache = new ConcurrentHashMap<>();
public void put(Order order) {
cache.put(order.getId(), order); // 问题:只增不减
}
}
解决方案:
// 方案1:使用Caffeine缓存,设置过期时间
private static final Cache<String, Order> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(30))
.build();
// 方案2:使用WeakHashMap
private static final Map<String, Order> cache = new WeakHashMap<>();
// 方案3:定期清理
@Scheduled(fixedRate = 3600000) // 每小时清理
public void cleanup() {
cache.entrySet().removeIf(e -> e.getValue().isExpired());
}
案例2:内存泄漏导致OOM
现象描述:
应用运行一段时间后抛出java.lang.OutOfMemoryError: Java heap space。
排查过程:
# 1. 开启OOM时自动dump(提前配置)
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/logs/oom.hprof
# 2. 使用MAT分析dump文件
# 3. 查看Dominator Tree,找到占用内存最大的对象
# 4. 查看引用链,找到GC Root
常见内存泄漏模式:
// 模式1:静态集合持有对象引用
public class LeakySingleton {
private static final List<byte[]> cache = new ArrayList<>();
public void addData(byte[] data) {
cache.add(data); // 永远不会被GC
}
}
// 模式2:监听器未注销
public class EventListener {
public void register() {
EventBus.register(this); // 忘记在销毁时unregister
}
}
// 模式3:ThreadLocal未清理
public class RequestContext {
private static final ThreadLocal<Map<String, Object>> context =
new ThreadLocal<>();
public void setContext(Map<String, Object> ctx) {
context.set(ctx); // 线程池环境下可能泄漏
}
// 需要 finally 中调用 context.remove()
}
// 模式4:数据库连接未关闭
public void query() {
Connection conn = dataSource.getConnection();
// 使用conn...
// 忘记关闭,连接对象无法被回收
}
案例3:CPU使用率异常高
现象描述: 应用CPU使用率持续100%,但业务处理量并没有增加。
排查过程:
# 1. 找到CPU高的Java进程
top -H -p <pid>
# 2. 找到CPU高的线程(假设线程ID为12345)
printf "%x\n" 12345 # 转换为16进制:3039
# 3. 查看线程堆栈
jstack <pid> | grep -A 20 "3039"
# 4. 发现是GC线程在消耗CPU
问题分析:
如果GC线程消耗大量CPU,通常是因为:
- 堆内存不足,频繁GC
- 内存泄漏,GC无法回收
- 对象创建过快
解决方案:
# 1. 增大堆内存
-Xmx4g -Xms4g
# 2. 分析GC日志,确认GC频率
-Xlog:gc*:file=gc.log:time,level,tags
# 3. 使用JFR持续监控
-XX:StartFlightRecording=filename=recording.jfr,settings=profile,duration=60s
案例4:应用启动慢
现象描述: 应用启动需要30秒以上,其中大部分时间在GC。
排查过程:
# 分析启动日志
-Xlog:gc+heap:debug:file=startup.log
# 发现大量类加载和初始化导致的GC
优化方案:
# 1. 使用G1或ZGC(更好的启动性能)
-XX:+UseG1GC
# 2. 调整年轻代大小
-XX:G1NewSizePercent=10
# 3. 使用类数据共享(CDS)
java -Xshare:dump # 生成共享归档
-Xshare:on # 启动时使用
# 4. ZGC的启动优化
-XX:+UseZGC -XX:+ZGenerational -XX:+AlwaysPreTouch
GC日志分析工具
使用GCEasy分析GC日志:
# 1. 生成GC日志
-Xlog:gc*:file=gc.log:time,level,tags:filecount=5,filesize=100m
# 2. 上传到 https://gceasy.io/ 分析
# 3. 查看报告中的关键指标:
# - GC暂停时间分布
# - 内存使用趋势
# - 关键问题和建议
关键GC指标解读:
| 指标 | 健康值 | 异常值 | 说明 |
|---|---|---|---|
| GC暂停时间占比 | < 5% | > 10% | GC时间占总运行时间比例 |
| Full GC频率 | < 1次/小时 | > 1次/分钟 | Full GC触发频率 |
| Young GC平均时间 | < 50ms | > 200ms | 年轻代GC平均耗时 |
| 老年代使用率 | < 70% | > 90% | 老年代内存使用率 |
| 对象晋升率 | 稳定 | 持续上升 | 对象从年轻代晋升到老年代的速率 |
常见GC问题排查
内存泄漏
对象不再使用但无法被回收:
public class MemoryLeak {
private static final List<Object> cache = new ArrayList<>();
public void addToCache(Object obj) {
cache.add(obj); // 对象无法被回收
}
}
解决方法:使用WeakHashMap或定期清理
内存溢出(OOM)
// java.lang.OutOfMemoryError: Java heap space
byte[] hugeArray = new byte[Integer.MAX_VALUE];
// java.lang.OutOfMemoryError: Metaspace
// 大量动态生成类
排查步骤:
- 开启堆转储:
-XX:+HeapDumpOnOutOfMemoryError - 分析dump文件:使用MAT、VisualVM等工具
- 定位内存泄漏或调整堆大小
Full GC频繁
可能原因:
- 老年代空间不足
- 方法区/元空间不足
- System.gc()调用
- 堆转储或死锁检测
解决方法:
- 调整堆大小
- 优化代码减少对象创建
- 禁用显式GC:
-XX:+DisableExplicitGC
小结
垃圾回收机制是JVM的核心功能:
- 判断对象存活:可达性分析算法
- 引用类型:强、软、弱、虚四种引用
- 垃圾收集算法:标记-清除、复制、标记-整理、分代收集
- 垃圾收集器:Serial、Parallel、CMS、G1、ZGC等
- 内存分配策略:Eden优先、大对象老年代、年龄晋升
- 问题排查:内存泄漏、OOM、Full GC频繁
理解GC机制有助于优化应用性能和排查内存问题。