Skip to content

垃圾回收算法与收集器深度解析

面试官:说说 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 的引用计数 = 1
b.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、WeakHashMapWeakReference<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("对象已回收");
}
}
}

为什么废弃:

  1. 不可靠:finalize() 执行时间不确定,甚至可能不执行
  2. 性能差:finalize() 由低优先级线程执行,大量对象会拖慢 GC
  3. 安全隐患:finalize() 中可能复活对象,破坏 GC 逻辑
  4. 替代方案:使用 try-with-resourcesCleaner(Java 9+)

本质一句话:finalize() 是对象的”临终遗言”,但不可靠且性能差,已被废弃,用显式资源管理替代。


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 + S0
Step 3: S0 和 S1 互换角色(下次 Eden + S1 → S0)

性能数据:

  • 每次 Minor GC 只扫描 Eden + 一个 Survivor(约 90% 新生代)
  • 存活对象复制到另一个 Survivor(约 10% 空间足够)
  • 内存浪费从 50% 降至 10%

为什么是 8:1:1:

经验值!IBM 研究表明,新生代对象存活率约 10%,所以 10% 的 Survivor 空间足够。可根据实际情况调整:

Terminal window
-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 的晋升担保失败
└── 老年代无法容纳新生代的存活对象(担保失败)

实战案例:

Terminal window
# 查看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 慢而致命,应尽量避免。


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(兜底,单线程,应尽量避免)

关键参数:

Terminal window
-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 对比:

G1ZGC
适用场景通用,兼顾吞吐量和停顿超大堆、对延迟极敏感
停顿时间可控(默认 200ms 以内)极低(< 10ms,甚至 < 1ms)
吞吐量较高略低(读屏障有额外开销)
堆大小数 GB ~ 数十 GB数 GB ~ 数 TB
内存开销约 20~40MB 额外开销约 15% 额外内存(着色指针等)
可用版本Java 9+(默认)Java 15+ 生产可用(11 实验)
STW 阶段初始标记 + 最终标记 + 清理初始标记 + 最终标记(极短)

性能数据:

  • ZGC 停顿时间与堆大小无关,始终 < 1ms
  • 读屏障吞吐量损失约 5~15%
  • 支持 TB 级堆,停顿仍 < 1ms

本质一句话:ZGC 用着色指针标记对象状态,用读屏障让用户线程”自愈”引用,将 STW 分散到每次读取,实现亚毫秒级停顿。


Q1:线上发现 Full GC 频繁,如何排查?实战

Section titled “Q1:线上发现 Full GC 频繁,如何排查?”

排查步骤:

Terminal window
# 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.5GB
4. 定位代码:
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:电商、推荐系统

Terminal window
# 特点:堆 8~16GB,延迟敏感,吞吐量也重要
# 推荐:G1
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100 # 目标停顿 100ms
-Xms8g -Xmx8g

场景 2:金融实时交易、游戏服务器

Terminal window
# 特点:延迟必须 < 10ms,堆可能 > 32GB
# 推荐:ZGC(Java 17+)
-XX:+UseZGC
-XX:SoftMaxHeapSize=32g # 软堆上限
-Xms32g -Xmx64g # 硬堆上限,预留弹性空间

场景 3:大数据处理(Spark、Flink)

Terminal window
# 特点:吞吐量优先,延迟不敏感
# 推荐: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. 选用低停顿收集器

Terminal window
# G1:停顿可控
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100 # 设置目标停顿
# ZGC:停顿 < 1ms
-XX:+UseZGC

2. 减少堆大小(矛盾!)

堆越小 → 标记范围越小 → STW 越短,但 GC 更频繁,吞吐量下降。需权衡。

3. 增加 GC 线程数

Terminal window
-XX:ParallelGCThreads=8 # 并行 GC 线程数
-XX:ConcGCThreads=2 # 并发 GC 线程数
# 充分利用多核,加快 STW 阶段

4. 减少对象晋升,避免 Full GC

Terminal window
-XX:MaxTenuringThreshold=15 # 增大晋升年龄(默认 15)
-XX:SurvivorRatio=6 # 增大 Survivor,减少晋升

5. 避免显式 GC 调用

Terminal window
-XX:+DisableExplicitGC # 禁用 System.gc()

本质一句话:STW 是 GC 的代价,现代收集器通过并发标记、读屏障等技术减少 STW,但无法完全消除。


GC 核心知识点:

  1. 垃圾判定:可达性分析从 GC Root 出发,不可达则为垃圾
  2. 引用类型:强/软/弱/虚引用,回收时机不同
  3. 三大算法:标记-清除(有碎片)、标记-复制(费空间)、标记-整理(费时间)
  4. 分代收集:新生代用标记-复制,老年代用标记-清除/整理
  5. GC 类型:Minor GC 快而频繁,Major GC 慢而少,Full GC 致命
  6. CMS:并发标记减少 STW,但内存碎片,CPU 敏感,已废弃
  7. G1:Region 化布局,Garbage First 优先回收高收益 Region,停顿可控
  8. ZGC:着色指针 + 读屏障,并发移动对象,停顿 < 1ms
  9. GC 调优:选择合适的收集器,调整堆大小,避免 Full GC
  10. 问题排查:jstat 监控 GC,jmap dump,MAT 分析泄漏

面试必答点:

  • 可达性分析 vs 引用计数(解决循环引用)
  • 四种引用类型及用途(软引用缓存,弱引用 ThreadLocal)
  • 三大算法优缺点及适用场景
  • Minor/Major/Full GC 区别与触发条件
  • CMS 的缺点与废弃原因
  • G1 的 Region 化与停顿预测模型
  • ZGC 的着色指针与读屏障原理
  • Full GC 排查步骤与工具
  • GC 收集器选型决策
  • STW 的本质与减少方法