垃圾回收算法与收集器深度解析
面试官:说说 JVM 如何判断对象可以被回收?
你:JVM 使用可达性分析,从 GC Roots 出发沿引用链遍历,不可达的对象即为垃圾。不使用引用计数,因为无法解决循环引用。
面试官:那 CMS 和 G1 有什么区别?G1 为什么能控制停顿时间?
这个问题考察的是对现代垃圾收集器的深度理解。能说清 G1 的 Region 化布局和停顿预测模型的候选人,才是真正的 JVM 高手。
链式追问一:垃圾判定与引用类型
Section titled “链式追问一:垃圾判定与引用类型”Q1:JVM 如何判断对象可以被回收?为什么不用引用计数?必考
Section titled “Q1:JVM 如何判断对象可以被回收?为什么不用引用计数?”可达性分析(Reachability Analysis):
从 GC Roots 出发,沿引用链遍历,能访问到的对象视为”可达”(存活),访问不到的视为垃圾候选:
GC Roots 包含:├── 虚拟机栈(栈帧)中引用的对象 ← 方法里的局部变量├── 方法区中类静态属性引用的对象 ← static 字段├── 方法区中常量引用的对象 ← final static 常量├── 本地方法栈中 JNI 引用的对象 ← native 方法└── JVM 内部引用(基本类型 Class、常驻异常类等)
可达 → 存活 不可达 → 垃圾候选(还需两次标记才真正死亡)为什么不用引用计数:
// 引用计数无法解决循环引用class Node { Node next;}
Node a = new Node();Node b = new Node();a.next = b; // b 的引用计数 = 1b.next = a; // a 的引用计数 = 1
a = null; // a 引用计数 = 1(b.next 还引用)b = null; // b 引用计数 = 1(a.next 还引用)// 两个对象都无法回收,但实际上都是垃圾!对比表格:
| 判定方式 | 原理 | 优点 | 缺点 | JVM 采用 |
|---|---|---|---|---|
| 引用计数 | 对象被引用时计数器+1,引用失效时-1,计数为0则回收 | 简单,实时回收 | 无法解决循环引用,计数器维护开销大 | 否 |
| 可达性分析 | 从 GC Root 遍历引用链,不可达则为垃圾 | 解决循环引用,准确 | 需要 STW,遍历有开销 | 是 |
本质一句话:可达性分析从 GC Root 出发”顺藤摸瓜”,不可达的对象是垃圾,解决了引用计数的循环引用问题。
Q2:Java 有哪几种引用类型?各自的特点和用途是什么?高频
Section titled “Q2:Java 有哪几种引用类型?各自的特点和用途是什么?”四种引用类型:
| 引用类型 | 回收时机 | 典型用途 | 实现类 |
|---|---|---|---|
| 强引用 | 永不回收(除非引用断开) | 普通对象 | Object o = new Object() |
| 软引用 | 内存不足时回收 | 图片缓存、重新创建代价大的对象 | SoftReference<T> |
| 弱引用 | 下次 GC 必然回收 | ThreadLocal 的 key、WeakHashMap | WeakReference<T> |
| 虚引用 | 随时,回收时会收到通知 | 跟踪对象回收,管理堆外内存 | PhantomReference<T> |
代码示例:
// 软引用:内存敏感的缓存SoftReference<byte[]> cache = new SoftReference<>(new byte[1024 * 1024]);byte[] data = cache.get(); // 获取对象if (data == null) { // 内存不足时已被回收,重新加载 data = loadData(); cache = new SoftReference<>(data);}
// 弱引用:ThreadLocal 的实现static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); // key 是弱引用 value = v; // value 是强引用 }}
// 虚引用:管理堆外内存PhantomReference<Object> ref = new PhantomReference<>(obj, queue);// 对象被回收后,ref 进入 queue,通过 queue.poll() 检测并清理堆外内存性能数据:
- 软引用:在堆占用接近上限时触发回收,典型回收率 30~50%
- 弱引用:每次 Minor GC 都回收,回收率接近 100%
- 虚引用:不阻止回收,仅用于通知,无性能数据
本质一句话:强引用绝对存活,软引用内存不足时回收,弱引用下次 GC 回收,虚引用用于回收通知。
Q3:finalize() 方法有什么作用?为什么废弃?中频
Section titled “Q3:finalize() 方法有什么作用?为什么废弃?”finalize() 的两次标记机制:
不可达对象不会立即死亡,还有最后一次”逃脱”机会:
第一次标记:可达性分析发现不可达,检查是否覆盖了 finalize()├── 未覆盖 / 已调用过 finalize() → 直接回收└── 覆盖了且未调用过 → 放入 F-Queue
第二次标记:JVM 低优先级线程执行 F-Queue 中的 finalize()├── 在 finalize() 中重新与 GC Root 关联 → 逃脱(本次不回收)└── 仍然不可达 → 回收
注意:finalize() 只会被调用一次,逃脱只能成功一次代码示例:
public class FinalizeEscape { public static FinalizeEscape SAVE_HOOK = null;
@Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize() executed"); SAVE_HOOK = this; // 重新关联 GC Root,逃脱死亡 }
public static void main(String[] args) throws Exception { SAVE_HOOK = new FinalizeEscape(); SAVE_HOOK = null; // 对象不可达 System.gc(); // 第一次 GC,finalize() 执行,对象逃脱 Thread.sleep(500); if (SAVE_HOOK != null) { System.out.println("对象存活"); }
SAVE_HOOK = null; // 再次不可达 System.gc(); // 第二次 GC,finalize() 不会再次执行,对象被回收 Thread.sleep(500); if (SAVE_HOOK == null) { System.out.println("对象已回收"); } }}为什么废弃:
- 不可靠:finalize() 执行时间不确定,甚至可能不执行
- 性能差:finalize() 由低优先级线程执行,大量对象会拖慢 GC
- 安全隐患:finalize() 中可能复活对象,破坏 GC 逻辑
- 替代方案:使用
try-with-resources或Cleaner(Java 9+)
本质一句话:finalize() 是对象的”临终遗言”,但不可靠且性能差,已被废弃,用显式资源管理替代。
链式追问二:三大垃圾回收算法
Section titled “链式追问二:三大垃圾回收算法”Q1:垃圾回收有哪三种基本算法?各自的优缺点是什么?必考
Section titled “Q1:垃圾回收有哪三种基本算法?各自的优缺点是什么?”标记-清除(Mark-Sweep):
标记阶段:从 GC Root 遍历,标记所有可达对象清除阶段:扫描整个堆,回收未标记对象
优点:简单,不需要移动对象缺点:产生大量不连续的内存碎片 → 大对象无法分配 → 提前触发 GC 标记和清除效率都不高(需要全堆扫描两次)标记-复制(Mark-Copy):
将内存分为两个等大的半区(From / To): From 区:正在使用 To 区:空的
GC 时: 标记 From 区的存活对象 将存活对象复制到 To 区(紧凑排列,无碎片) 清空整个 From 区 From 和 To 互换
优点:无内存碎片;分配简单(指针碰撞)缺点:可用内存只有一半(浪费);存活对象多时复制开销大标记-整理(Mark-Compact):
标记阶段:同标记-清除整理阶段:将存活对象向内存一端移动,然后清理边界以外的内存
优点:无内存碎片;可用内存 100%缺点:移动对象需要更新所有引用(停顿时间长) 适合老年代(存活对象多,复制代价高)对比表格:
| 算法 | 内存碎片 | 内存利用率 | 移动对象 | STW 时长 | 适用分代 |
|---|---|---|---|---|---|
| 标记-清除 | 有 | 100% | 否 | 短 | 老年代(CMS) |
| 标记-复制 | 无 | 50% | 是 | 中 | 新生代(Serial、ParNew) |
| 标记-整理 | 无 | 100% | 是 | 长 | 老年代(Serial Old、Parallel Old) |
本质一句话:标记-清除有碎片,标记-复制费空间,标记-整理费时间,各有利弊,分代收集各取所长。
Q2:新生代为什么用标记-复制算法?Eden:S0:S1 为什么是 8:1:1?高频
Section titled “Q2:新生代为什么用标记-复制算法?Eden:S0:S1 为什么是 8:1:1?”新生代对象特点:
根据弱分代假说,新生代对象 98% 都是”朝生夕死”,存活率极低:
对象生命周期分布:├── 90%+ 对象存活时间 < 1 次 Minor GC├── 8% 对象存活时间 1~15 次 Minor GC(晋升老年代)└── 2% 对象长期存活(进入老年代)标记-复制的优化:
对象 98% 朝生夕死,不必 1:1 划分。HotSpot 使用 Eden:S0:S1 = 8:1:1,每次只浪费 10%:
新生代布局(10 份):├── Eden(8 份) ← 新对象出生地,占 80%├── Survivor 0(1 份) ← 存活对象暂存区└── Survivor 1(1 份) ← 存活对象暂存区
GC 流程:Step 1: Eden + S0 存活对象 → 复制到 S1(紧凑排列)Step 2: 清空 Eden + S0Step 3: S0 和 S1 互换角色(下次 Eden + S1 → S0)性能数据:
- 每次 Minor GC 只扫描 Eden + 一个 Survivor(约 90% 新生代)
- 存活对象复制到另一个 Survivor(约 10% 空间足够)
- 内存浪费从 50% 降至 10%
为什么是 8:1:1:
经验值!IBM 研究表明,新生代对象存活率约 10%,所以 10% 的 Survivor 空间足够。可根据实际情况调整:
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1(默认)-XX:SurvivorRatio=6 # Eden:S0:S1 = 6:1:1,增大 Survivor,减少晋升本质一句话:新生代用标记-复制是因为对象存活率低,8:1:1 是经验值,保证 90% 空间用于分配,10% 空间足够存存活对象。
Q3:Minor GC、Major GC、Full GC 有什么区别?什么时候触发 Full GC?必考
Section titled “Q3:Minor GC、Major GC、Full GC 有什么区别?什么时候触发 Full GC?”GC 类型对比:
| GC 类型 | 回收范围 | 触发条件 | 停顿时间 | 频率 |
|---|---|---|---|---|
| Minor GC | 新生代 | Eden 区满 | 短(通常 ms 级) | 高(几秒一次) |
| Major GC | 老年代 | 老年代空间不足 | 较长(数百 ms) | 中 |
| Full GC | 整个堆 + 方法区 | 老年代满、System.gc() 等 | 长(可能秒级) | 低(应避免) |
Full GC 触发条件(面试常问):
1. 老年代空间不足 ├── 对象晋升时老年代无空间 ├── 大对象直接进入老年代(-XX:PretenureSizeThreshold) └── 动态年龄判断:Survivor 同年龄对象超 50%,直接晋升
2. 调用 System.gc() └── 建议 GC,不保证立即执行(可 -XX:+DisableExplicitGC 禁用)
3. 元空间(Metaspace)满 └── 类加载过多,元空间达到上限
4. CMS GC 的 Concurrent Mode Failure └── 并发 GC 时老年代满,退化为 Serial Old,停顿极长
5. Minor GC 的晋升担保失败 └── 老年代无法容纳新生代的存活对象(担保失败)实战案例:
# 查看GC统计jstat -gcutil <pid> 1000 10# 输出:# S0 S1 E O M YGC YGCT FGC FGCT GCT# 0.00 12.5 85.3 92.1 45.2 1234 12.345 5 3.456 15.801# ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑# S0使用 S1使用 Eden 老年代 元空间 YGC次数 YGC耗时 FGC次数 FGC耗时 总GC耗时
# 判断:# O(老年代)持续 > 90% → 可能即将 Full GC# FGC 快速增加 → 需立即排查本质一句话:Minor GC 快而频繁,Major GC 慢而少,Full GC 慢而致命,应尽量避免。
链式追问三:经典与现代收集器
Section titled “链式追问三:经典与现代收集器”Q1:CMS 收集器的工作流程是什么?有什么缺点?高频
Section titled “Q1:CMS 收集器的工作流程是什么?有什么缺点?”CMS(Concurrent Mark Sweep)工作流程:
1. 初始标记(STW,极短) └── 标记 GC Root 直接引用的对象
2. 并发标记(与用户线程并发) └── 从直接引用出发,遍历整个引用链
3. 重新标记(STW,短) └── 修正并发标记阶段用户程序修改的引用(增量更新)
4. 并发清除(与用户线程并发) └── 清除垃圾对象(标记-清除,不移动对象)CMS 的缺点:
| 缺点 | 说明 | 影响 |
|---|---|---|
| 内存碎片 | 使用标记-清除,不移动对象 | 大对象无法分配,提前 Full GC |
| CPU 敏感 | 并发阶段占用 CPU | 降低吞吐量(约 25% CPU 给 GC 线程) |
| 浮动垃圾 | 并发标记阶段新产生的垃圾 | 本次 GC 不处理,浪费空间 |
| Concurrent Mode Failure | 并发 GC 时老年代满 | 退化为 Serial Old,停顿极长 |
性能数据:
- 初始标记 + 重新标记:约 10~50ms(STW)
- 并发标记 + 并发清除:与堆大小成正比,但用户线程继续运行
- Concurrent Mode Failure 退化为 Serial Old:停顿可能数秒
本质一句话:CMS 用并发标记减少 STW,但代价是内存碎片和 CPU 开销,已废弃,被 G1 替代。
Q2:G1 收集器为什么能控制停顿时间?它是如何实现的?必考
Section titled “Q2:G1 收集器为什么能控制停顿时间?它是如何实现的?”G1 的核心创新:Region 化堆布局:
将堆划分为等大的 Region(默认 1MB~32MB),不再固定分代边界:
传统分代: G1 Region 模型:┌────────┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐│ Eden │ │E│E│S│O│O│H│E│O│ E=Eden S=Survivor├────────┤ ├─┼─┼─┼─┼─┼─┼─┼─┤ O=Old H=Humongous(大对象)│ S0/S1 │ │O│E│O│E│S│O│O│H│├────────┤ └─┴─┴─┴─┴─┴─┴─┴─┘│ Old │ 每个 Region 可动态扮演不同角色└────────┘ 大对象(> Region 大小 50%)直接放 Humongous Region停顿时间可控的三大机制:
1. Region 化 + 优先回收垃圾最多的 Region(Garbage First)
G1 在并发标记阶段统计每个 Region 的存活比例和回收价值。GC 时在目标停顿时间内,优先选取收益最大(垃圾最多)的 Region 组合:
Region 回收价值计算: 价值 = (垃圾空间 / 回收耗时) × 紧急程度
停顿时间预测: 根据 GC 历史,预测每个 Region 的回收耗时 在目标停顿时间内,选取价值最高的 Region 组合2. 自适应调整年轻代大小
G1 根据历史 GC 数据,动态调整年轻代 Region 数量,使 Young GC 停顿时间接近 -XX:MaxGCPauseMillis 设置的目标。
3. 并发标记减少 STW
大部分标记工作与用户线程并发执行,只有初始标记和最终标记需要短暂 STW。
G1 的 GC 流程:
1. 年轻代 GC(Young GC,STW) └── 回收所有 Eden 和 Survivor Region
2. 并发标记(与用户线程并发) ├── 初始标记(STW,借用 Young GC) ├── 并发标记 ├── 最终标记(STW,SATB 处理) └── 清理(STW):统计各 Region 存活比例
3. 混合 GC(Mixed GC) └── 回收所有新生代 + 垃圾最多的部分老年代 Region(GarbageFirst 的含义)
4. Full GC(兜底,单线程,应尽量避免)关键参数:
-XX:+UseG1GC # 开启 G1-XX:MaxGCPauseMillis=200 # 目标最大停顿时间(默认200ms)-XX:G1HeapRegionSize=4m # Region 大小(1~32MB,2的幂)-XX:G1NewSizePercent=5 # 年轻代最小占比(默认5%)-XX:G1MaxNewSizePercent=60 # 年轻代最大占比(默认60%)本质一句话:G1 用 Region 化打破分代边界,用 Garbage First 优先回收收益最大的 Region,实现停顿时间可控。
Q3:ZGC 为什么能实现 < 1ms 停顿?着色指针和读屏障是什么?加分
Section titled “Q3:ZGC 为什么能实现 < 1ms 停顿?着色指针和读屏障是什么?”传统 GC 的停顿瓶颈:
移动对象时需要更新所有指向它的引用,这些引用散布在堆的各处,必须 STW 才能安全完成,停顿时间与堆大小成正比。
ZGC 的核心创新:
1. 着色指针(Colored Pointer):
64 位地址结构:┌─────────┬──────────────────────────────────┐│ 颜色位 │ 对象地址(42位) ││ (4位) │ │└─────────┴──────────────────────────────────┘ ↑ Marked0/Marked1/Remapped/Finalizable
作用: GC 线程移动对象后,修改指针颜色 旧指针颜色变为"过期",新位置用新颜色标记2. 读屏障(Load Barrier):
JIT 在每个对象引用读取操作后插入一小段检查代码:
Object o = obj.field; // 读取引用// JIT 插入读屏障:if (o 的颜色不是当前期望值) { // 对象已被移动,通过转发表找到新地址 o = 转发表.get(o); obj.field = o; // 原子更新引用(自愈)}并发移动对象流程:
Step 1: GC 线程移动对象 A 到新位置 └── 旧位置 A 的指针颜色变为"过期"
Step 2: 用户线程读取 obj.field(指向旧位置 A) └── 读屏障检测到指针颜色过期 └── 自动通过转发表找到新位置 └── 原子更新 obj.field 指向新位置(自愈)
Step 3: 多次读取后,所有引用都更新到新位置 └── 旧位置 A 无引用,可回收G1 vs ZGC 对比:
| G1 | ZGC | |
|---|---|---|
| 适用场景 | 通用,兼顾吞吐量和停顿 | 超大堆、对延迟极敏感 |
| 停顿时间 | 可控(默认 200ms 以内) | 极低(< 10ms,甚至 < 1ms) |
| 吞吐量 | 较高 | 略低(读屏障有额外开销) |
| 堆大小 | 数 GB ~ 数十 GB | 数 GB ~ 数 TB |
| 内存开销 | 约 20~40MB 额外开销 | 约 15% 额外内存(着色指针等) |
| 可用版本 | Java 9+(默认) | Java 15+ 生产可用(11 实验) |
| STW 阶段 | 初始标记 + 最终标记 + 清理 | 初始标记 + 最终标记(极短) |
性能数据:
- ZGC 停顿时间与堆大小无关,始终 < 1ms
- 读屏障吞吐量损失约 5~15%
- 支持 TB 级堆,停顿仍 < 1ms
本质一句话:ZGC 用着色指针标记对象状态,用读屏障让用户线程”自愈”引用,将 STW 分散到每次读取,实现亚毫秒级停顿。
链式追问四:GC 调优与问题排查
Section titled “链式追问四:GC 调优与问题排查”Q1:线上发现 Full GC 频繁,如何排查?实战
Section titled “Q1:线上发现 Full GC 频繁,如何排查?”排查步骤:
# Step 1: 查看 GC 概况jstat -gcutil <pid> 1000 10# 输出:S0 S1 E O M YGC YGCT FGC FGCT GCT# 重点关注:# O(老年代使用率)持续增长 → 内存泄漏# FGC(Full GC 次数)快速增加 → 需立即排查# FGCT(Full GC 总耗时)/ FGC = 单次 Full GC 耗时
# Step 2: 查看 GC 日志(启动时加参数)# Java 11+-Xlog:gc*:file=/tmp/gc.log:time,uptime,level,tags
# 分析 GC 日志中的 Full GC cause:# "Allocation Failure" → 老年代空间不足,可能内存泄漏或堆太小# "Metadata GC Threshold" → 元空间满,类加载过多# "System.gc()" → 代码显式调用,排查调用方
# Step 3: 查看存活对象分布jmap -histo:live <pid> | head -30# 输出:实例数量 × 字节大小 × 类名# 找到实例数最多的类
# Step 4: dump 堆快照(生产环境慎用,会 STW)jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
# Step 5: 用 Eclipse MAT 分析# - 查看 Dominator Tree(哪些对象占用最多内存)# - 找到 GC Root 到泄漏对象的引用链# - 定位代码位置常见原因与解法:
| 原因 | 现象 | 解法 |
|---|---|---|
| 内存泄漏 | 老年代持续增长,GC 后不降 | MAT 分析找到泄漏对象,修复代码 |
| 堆太小 | 老年代经常满,但 GC 后可回收 | 增大堆(-Xmx) |
| 大对象过多 | 老年代占用高,对象年龄低 | 优化代码,减少大对象 |
| 元空间满 | 类加载过多,元空间 OOM | 监控类数量,检查动态代理/热部署 |
| System.gc() | GC 日志显示显式调用 | -XX:+DisableExplicitGC 禁用 |
实战案例:
场景:某电商系统每 10 分钟 Full GC 一次,每次停顿 3 秒
排查:1. jstat -gcutil 显示老年代使用率持续 95%+2. jmap -histo:live 显示 ConcurrentHashMap$Node 实例数 100万+3. dump 堆快照,MAT 分析发现某静态缓存 Map 占用 1.5GB4. 定位代码: static Map<String, Object> cache = new ConcurrentHashMap<>(); void addToCache(String key, Object value) { cache.put(key, value); // 只加不删! }
修复:改用 Caffeine 缓存,设置过期时间 Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.MINUTES) .maximumSize(10000) .build();
效果:Full GC 频率降至每天 1 次,停顿 < 500ms本质一句话:Full GC 排查是”看现象 → 查日志 → 找大对象 → 追 GC Root”,核心是用 MAT 定位泄漏对象。
Q2:如何选择垃圾收集器?G1、ZGC、Parallel GC 各适合什么场景?实战
Section titled “Q2:如何选择垃圾收集器?G1、ZGC、Parallel GC 各适合什么场景?”收集器对比:
| 收集器 | 核心目标 | 适用场景 | 堆大小 | 停顿时间 | 吞吐量 |
|---|---|---|---|---|---|
| Serial | 简单,低内存 | 客户端应用、单核机器 | < 100MB | 长(秒级) | 低 |
| Parallel | 高吞吐量 | 批处理、数据分析、科学计算 | 数 GB | 长(数百 ms) | 高 |
| CMS | 低停顿(已废弃) | 旧系统兼容 | 数 GB | 短(数十 ms) | 中 |
| G1 | 平衡吞吐量与停顿 | 通用 Web 服务、微服务 | 4~32GB | 可控(默认 200ms) | 较高 |
| ZGC | 超低停顿 | 实时交易、游戏、超大堆 | 数 GB ~ 数 TB | 极短(< 1ms) | 中 |
| Shenandoah | 超低停顿 | 实时系统、低延迟 API | 数 GB ~ 数 TB | 极短(< 10ms) | 中 |
选型决策树:
问题:是否需要超低延迟(< 10ms)?├── 是 → 用 ZGC 或 Shenandoah│ └── 堆 > 100GB 或延迟 < 1ms → ZGC│ └── 堆 < 100GB 或延迟 < 10ms → Shenandoah└── 否 → 用 G1 或 Parallel ├── 问题:吞吐量更重要还是延迟更重要? │ ├── 吞吐量 → Parallel GC │ └── 延迟 → G1 └── 问题:堆大小? ├── < 4GB → G1 或 Parallel └── 4~32GB → G1实战建议:
场景 1:电商、推荐系统
# 特点:堆 8~16GB,延迟敏感,吞吐量也重要# 推荐:G1-XX:+UseG1GC-XX:MaxGCPauseMillis=100 # 目标停顿 100ms-Xms8g -Xmx8g场景 2:金融实时交易、游戏服务器
# 特点:延迟必须 < 10ms,堆可能 > 32GB# 推荐:ZGC(Java 17+)-XX:+UseZGC-XX:SoftMaxHeapSize=32g # 软堆上限-Xms32g -Xmx64g # 硬堆上限,预留弹性空间场景 3:大数据处理(Spark、Flink)
# 特点:吞吐量优先,延迟不敏感# 推荐:Parallel GC-XX:+UseParallelGC-XX:GCTimeRatio=99 # GC 时间占比不超过 1/(1+99)=1%-XX:MaxGCPauseMillis=500 # 允许较长停顿,换吞吐量本质一句话:Parallel 追吞吐,G1 求平衡,ZGC 极低停顿,根据业务延迟和吞吐需求选择。
Q3:什么是 STW?哪些阶段需要 STW?如何减少 STW 影响?高频
Section titled “Q3:什么是 STW?哪些阶段需要 STW?如何减少 STW 影响?”STW(Stop The World)定义:
GC 发生时,JVM 暂停所有用户线程(SafePoint),让 GC 线程独占 CPU 完成标记/整理工作。这段时间内应用完全不响应,表现为延迟飙高。
为什么需要 STW:
GC 做可达性分析需要一个一致的内存快照——如果用户线程在标记过程中修改引用关系,会导致标记错误(漏标死亡对象、漏标存活对象)。
各收集器 STW 情况:
| 收集器 | STW 阶段 | 典型停顿 |
|---|---|---|
| Serial/Parallel | 全程 | 几百 ms~几秒 |
| CMS | 初始标记 + 重新标记 | 数十 ms |
| G1 | 初始标记 + 最终标记 + 清理 | 数十~200ms |
| ZGC | 初始标记 + 最终标记 | < 1ms |
减少 STW 的方法:
1. 选用低停顿收集器
# G1:停顿可控-XX:+UseG1GC-XX:MaxGCPauseMillis=100 # 设置目标停顿
# ZGC:停顿 < 1ms-XX:+UseZGC2. 减少堆大小(矛盾!)
堆越小 → 标记范围越小 → STW 越短,但 GC 更频繁,吞吐量下降。需权衡。
3. 增加 GC 线程数
-XX:ParallelGCThreads=8 # 并行 GC 线程数-XX:ConcGCThreads=2 # 并发 GC 线程数# 充分利用多核,加快 STW 阶段4. 减少对象晋升,避免 Full GC
-XX:MaxTenuringThreshold=15 # 增大晋升年龄(默认 15)-XX:SurvivorRatio=6 # 增大 Survivor,减少晋升5. 避免显式 GC 调用
-XX:+DisableExplicitGC # 禁用 System.gc()本质一句话:STW 是 GC 的代价,现代收集器通过并发标记、读屏障等技术减少 STW,但无法完全消除。
GC 核心知识点:
- 垃圾判定:可达性分析从 GC Root 出发,不可达则为垃圾
- 引用类型:强/软/弱/虚引用,回收时机不同
- 三大算法:标记-清除(有碎片)、标记-复制(费空间)、标记-整理(费时间)
- 分代收集:新生代用标记-复制,老年代用标记-清除/整理
- GC 类型:Minor GC 快而频繁,Major GC 慢而少,Full GC 致命
- CMS:并发标记减少 STW,但内存碎片,CPU 敏感,已废弃
- G1:Region 化布局,Garbage First 优先回收高收益 Region,停顿可控
- ZGC:着色指针 + 读屏障,并发移动对象,停顿 < 1ms
- GC 调优:选择合适的收集器,调整堆大小,避免 Full GC
- 问题排查:jstat 监控 GC,jmap dump,MAT 分析泄漏
面试必答点:
- 可达性分析 vs 引用计数(解决循环引用)
- 四种引用类型及用途(软引用缓存,弱引用 ThreadLocal)
- 三大算法优缺点及适用场景
- Minor/Major/Full GC 区别与触发条件
- CMS 的缺点与废弃原因
- G1 的 Region 化与停顿预测模型
- ZGC 的着色指针与读屏障原理
- Full GC 排查步骤与工具
- GC 收集器选型决策
- STW 的本质与减少方法