一、垃圾回收算法基础
1. 请详细说明主流的垃圾回收算法及其优缺点
问题分析角度:
- 考察对GC算法理论基础的掌握
- 考察算法适用场景的判断能力
- 考察算法演进过程的理解
1.1 标记-清除算法(Mark-Sweep)
算法原理:
- 标记阶段: 标记所有需要回收的对象
- 清除阶段: 统一回收所有被标记的对象
java
// 伪代码示例
void markSweep() {
// 标记阶段
for (Object root : gcRoots) {
mark(root);
}
// 清除阶段
for (Object obj : heap) {
if (!isMarked(obj)) {
free(obj);
}
}
}
void mark(Object obj) {
if (obj == null || isMarked(obj)) return;
setMarked(obj);
for (Object ref : obj.getReferences()) {
mark(ref);
}
}
优点:
- 实现简单
- 不需要移动对象
缺点:
-
效率问题: 标记和清除效率都不高,需要扫描整个堆
-
空间问题: 产生大量内存碎片,可能导致大对象无法分配
回收前: [A][B][C][D][E][F]
回收后: [A][ ][C][ ][E][ ] // 产生碎片
应用场景: CMS收集器的老年代回收
1.2 标记-复制算法(Mark-Copy)
算法原理:
- 将内存分为大小相等的两块
- 使用其中一块,满了就将存活对象复制到另一块
- 清空已使用的那块内存
java
// 伪代码示例
void copyingGC() {
Object[] newSpace = allocateNewSpace();
int newIndex = 0;
// 复制所有存活对象
for (Object root : gcRoots) {
newIndex = copy(root, newSpace, newIndex);
}
// 交换空间
swapSpaces();
}
int copy(Object obj, Object[] newSpace, int index) {
if (obj == null || obj.forwardingPointer != null) {
return index;
}
// 复制对象到新空间
newSpace[index] = obj.clone();
obj.forwardingPointer = newSpace[index];
// 更新引用
for (Object ref : obj.getReferences()) {
index = copy(ref, newSpace, index + 1);
}
return index;
}
优点:
- 实现简单
- 运行高效
- 没有内存碎片
缺点:
- 内存利用率低(只能使用50%)
- 存活对象多时效率降低
优化方案 - Appel式回收:
新生代划分: Eden(80%) + Survivor0(10%) + Survivor1(10%)
[Eden 80%][S0 10%][S1 10%]
工作流程:
1. 对象优先在Eden分配
2. Eden满时,触发Minor GC
3. 存活对象复制到空闲的Survivor
4. 清空Eden和使用过的Survivor
实际效果:
java
// 新生代对象存活率通常很低(研究表明<10%)
// 使用8:1:1的比例,实际可用内存达到90%
-XX:SurvivorRatio=8 // Eden/Survivor=8
-XX:+UseAdaptiveSizePolicy // 动态调整比例
应用场景: 新生代回收(Serial、ParNew、Parallel Scavenge、G1的Young GC)
1.3 标记-整理算法(Mark-Compact)
算法原理:
- 标记阶段: 标记所有存活对象
- 整理阶段: 将所有存活对象移动到一端,清理边界外的内存
java
// 伪代码示例
void markCompact() {
// 标记阶段
for (Object root : gcRoots) {
mark(root);
}
// 计算新地址
int newAddress = heapStart;
for (Object obj : heap) {
if (isMarked(obj)) {
obj.forwardingAddress = newAddress;
newAddress += obj.size();
}
}
// 更新引用
updateReferences();
// 移动对象
for (Object obj : heap) {
if (isMarked(obj)) {
move(obj, obj.forwardingAddress);
}
}
}
优点:
- 没有内存碎片
- 不浪费空间
- 适合老年代(对象存活率高)
缺点:
- 移动对象成本高
- 需要暂停用户线程(Stop The World)
性能对比:
场景: 100MB堆,存活率90%
标记-复制: 需要复制90MB对象
标记-整理: 需要移动对象并更新引用,但不浪费空间
老年代选择标记-整理是因为:
1. 存活率高,复制成本大
2. 不能浪费50%的空间
应用场景: 老年代回收(Serial Old、Parallel Old、G1的Mixed GC)
1.4 分代收集理论
核心假说:
- 弱分代假说: 绝大多数对象都是朝生夕灭
- 强分代假说: 熬过多次GC的对象越难消亡
- 跨代引用假说: 跨代引用相对同代引用占极少数
分代设计:
堆内存结构:
+----------------------------------+
| 新生代(Young Gen) |
| Eden | Survivor0 | Survivor1|
| (8) | (1) | (1) |
+----------------------------------+
| 老年代(Old Gen) |
| |
+----------------------------------+
对象晋升规则:
1. 大对象直接进入老年代
-XX:PretenureSizeThreshold=3m
2. 长期存活对象进入老年代
-XX:MaxTenuringThreshold=15
3. 动态年龄判定
Survivor中相同年龄对象总大小 > Survivor空间一半
则年龄>=该年龄的对象进入老年代
4. 空间分配担保
Minor GC前检查老年代最大可用连续空间
是否大于新生代所有对象总空间
实战案例:
java
public class GenerationExample {
// 大对象直接进入老年代
byte[] bigObject = new byte[4 * 1024 * 1024]; // 4MB
public static void main(String[] args) {
// 短命对象-在新生代回收
for (int i = 0; i < 1000000; i++) {
String temp = new String("temp" + i);
}
// 长命对象-晋升到老年代
List<String> cache = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
cache.add(new String("cache" + i));
}
// 触发多次Minor GC后,cache对象会晋升到老年代
for (int i = 0; i < 10; i++) {
System.gc();
}
}
}
二、垃圾收集器详解
2. 请详细对比各种垃圾收集器的特点和适用场景
问题分析角度:
- 考察对各种收集器的深入理解
- 考察收集器选择的实战经验
- 考察新一代收集器的认知
2.1 Serial收集器(串行收集器)
特点:
-
单线程收集
-
新生代使用标记-复制算法
-
老年代使用标记-整理算法
-
收集时必须Stop The World
工作流程:
用户线程: ||||----[STW]----||||
Serial GC: [GC]
参数配置:
bash
-XX:+UseSerialGC # 启用Serial收集器
-XX:+UseSerialGC -XX:+UseSerialOldGC # 新生代+老年代都使用Serial
优点:
- 简单高效
- 单线程环境下停顿时间最短
- 内存占用小
缺点:
- 多核CPU下性能浪费
- 停顿时间较长
适用场景:
- Client模式下的默认收集器
- 单核CPU或内存受限环境
- 桌面应用程序
2.2 ParNew收集器(并行收集器)
特点:
-
Serial的多线程版本
-
新生代并行,老年代串行
-
可与CMS配合使用
工作流程:
用户线程: ||||----[STW]----||||
ParNew GC: [T1][T2][T3]
参数配置:
bash
-XX:+UseParNewGC # 启用ParNew
-XX:ParallelGCThreads=4 # 设置并行线程数(默认=CPU核数)
性能对比:
java
// 测试场景: 4核CPU,2GB堆
// Serial: 停顿200ms
// ParNew: 停顿80ms (提升60%)
// 但在单核环境下,ParNew可能慢于Serial
// 因为线程切换开销
适用场景:
- 多核CPU环境
- 配合CMS使用(JDK9前唯一选择)
2.3 Parallel Scavenge收集器(吞吐量优先)
特点:
- 关注吞吐量而非停顿时间
- 提供自适应调节策略
- 新生代收集器
核心概念:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间)
例如: 程序运行100分钟,GC 1分钟
吞吐量 = 99 / 100 = 99%
参数配置:
bash
-XX:+UseParallelGC # 启用Parallel Scavenge
-XX:MaxGCPauseMillis=100 # 最大停顿时间100ms
-XX:GCTimeRatio=99 # 吞吐量目标99%
-XX:+UseAdaptiveSizePolicy # 自适应调节(默认开启)
# 自适应调节会自动调整:
# -Xmn(新生代大小)
# -XX:SurvivorRatio(Eden/Survivor比例)
# -XX:PretenureSizeThreshold(大对象阈值)
吞吐量 vs 停顿时间:
高吞吐量场景:
- 后台计算任务
- 批处理作业
- 科学计算
低停顿时间场景:
- Web应用
- 交互式应用
- SLA要求高的服务
实战案例:
java
// 场景: 大数据批处理
// 需求: 最大化CPU利用率,可以接受较长的停顿
java -Xmx10g -Xms10g \
-XX:+UseParallelGC \
-XX:GCTimeRatio=99 \
-XX:+UseAdaptiveSizePolicy \
-jar data-processor.jar
// 结果: 吞吐量从95%提升到99%
适用场景:
- 后台计算密集型应用
- 批处理任务
- 对停顿时间不敏感的场景
2.4 CMS收集器(Concurrent Mark Sweep)
设计目标: 获取最短停顿时间
工作流程(四个阶段):
1. 初始标记(Initial Mark) - STW
||||----[STW]----||||
标记GC Roots直接关联的对象(速度很快)
2. 并发标记(Concurrent Mark)
用户线程: ||||||||||||||||
CMS线程: [标记整个引用链]
与用户线程并发执行
3. 重新标记(Remark) - STW
||||----[STW]----||||
修正并发标记期间变动的对象(停顿时间较长)
4. 并发清除(Concurrent Sweep)
用户线程: ||||||||||||||||
CMS线程: [清除未标记对象]
与用户线程并发执行
参数配置:
bash
-XX:+UseConcMarkSweepGC # 启用CMS
-XX:CMSInitiatingOccupancyFraction=75 # 老年代使用75%触发CMS
-XX:+UseCMSCompactAtFullCollection # Full GC后进行碎片整理
-XX:CMSFullGCsBeforeCompaction=0 # 多少次Full GC后整理
-XX:+CMSParallelRemarkEnabled # 并行重新标记
-XX:+CMSScavengeBeforeRemark # Remark前先做Minor GC
优点:
- 并发收集,停顿时间短
- 适合对响应时间敏感的应用
缺点:
- CPU资源敏感
java
// CMS默认启动线程数 = (CPU核数 + 3) / 4
// 4核CPU: (4+3)/4 = 1个线程,占用25% CPU
// 2核CPU: (2+3)/4 = 1个线程,占用50% CPU
// 解决方案:
-XX:ParallelCMSThreads=2 // 手动设置CMS线程数
- 浮动垃圾(Floating Garbage)
java
// 并发清除阶段产生的新垃圾无法回收
// 需要预留空间给用户线程
// 如果预留空间不足,触发"Concurrent Mode Failure"
// 启用后备方案Serial Old(Full GC),停顿时间很长
// 优化:
-XX:CMSInitiatingOccupancyFraction=70 // 降低触发阈值
- 内存碎片
java
// 使用标记-清除算法,产生大量碎片
// 可能导致大对象无法分配,触发Full GC
// 解决方案:
-XX:+UseCMSCompactAtFullCollection // Full GC时整理碎片
-XX:CMSFullGCsBeforeCompaction=5 // 5次Full GC后整理
实战案例:
bash
# 电商网站配置(8核16G服务器)
JAVA_OPTS="
-Xms8g -Xmx8g
-Xmn3g
-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyFraction
-XX:CMSInitiatingOccupancyFraction=70
-XX:+CMSParallelRemarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
"
# 效果:
# GC停顿时间: 50-100ms
# 吞吐量: 95%
# Full GC频率: 每天1-2次
适用场景:
- 互联网应用
- Web服务器
- 对响应时间敏感的系统
2.5 G1收集器(Garbage First)
设计理念:
- 面向服务端应用
- 兼顾吞吐量和停顿时间
- 可预测的停顿时间模型
内存布局革新:
传统分代:
+------------------+------------------+
| 新生代(连续) | 老年代(连续) |
+------------------+------------------+
G1分代:
+---+---+---+---+---+---+---+---+
| E | E | S | O | O | E | H | O | // Region混合布局
+---+---+---+---+---+---+---+---+
E=Eden, S=Survivor, O=Old, H=Humongous
每个Region: 1MB-32MB(必须是2的幂)
Humongous: 存储大对象(>= Region的50%)
核心概念 - Remembered Set(记忆集):
java
// 问题: 如何避免扫描整个堆来确定对象存活?
// 解决: 每个Region维护一个RSet,记录外部指向本Region的引用
Region A的RSet:
- Region B的对象引用A中的对象
- Region D的对象引用A中的对象
// Minor GC时只需扫描:
// 1. GC Roots
// 2. RSet中记录的引用
工作流程:
1. 初始标记(Initial Mark) - STW
附着在Minor GC中执行,标记GC Roots
2. 并发标记(Concurrent Mark)
从GC Roots开始对堆中对象进行可达性分析
与用户线程并发
3. 最终标记(Final Mark) - STW
处理并发标记阶段遗留的SATB记录
4. 筛选回收(Live Data Counting and Evacuation) - STW
对各Region回收价值排序
根据期望停顿时间制定回收计划
复制存活对象到空Region
参数配置:
bash
-XX:+UseG1GC # 启用G1
-XX:MaxGCPauseMillis=200 # 期望最大停顿时间200ms
-XX:G1HeapRegionSize=16m # Region大小16MB
-XX:InitiatingHeapOccupancyPercent=45 # 堆占用45%触发并发标记
-XX:G1NewSizePercent=5 # 新生代最小占比5%
-XX:G1MaxNewSizePercent=60 # 新生代最大占比60%
-XX:G1ReservePercent=10 # 保留空闲Region 10%
-XX:ConcGCThreads=4 # 并发GC线程数
-XX:ParallelGCThreads=8 # 并行GC线程数
Mixed GC详解:
java
// 当老年代占用达到阈值,触发Mixed GC
// Mixed GC会回收部分老年代Region
// G1根据价值优先选择回收的Region:
// 价值 = 回收收益 / 回收时间
回收收益 = Region中垃圾对象占用的空间
回收时间 = 复制存活对象所需时间
// 示例:
Region A: 90%垃圾, 需要10ms
Region B: 50%垃圾, 需要20ms
Region A价值 = 90% / 10ms = 9
Region B价值 = 50% / 20ms = 2.5
// 优先回收Region A
实战调优案例:
bash
# 场景: 4GB堆,要求停顿时间<100ms
# 初始配置
-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
# 问题: Young GC频繁,停顿时间超标
# 分析GC日志:
[GC pause (G1 Evacuation Pause) (young), 0.15 secs]
# 新生代太小,频繁触发GC
# 优化:
-XX:G1NewSizePercent=30 # 增加新生代最小占比
-XX:G1MaxNewSizePercent=50 # 限制新生代最大占比
-XX:MaxGCPauseMillis=80 # 降低目标停顿时间
# 结果:
# Young GC频率降低50%
# 平均停顿时间: 70ms
# 吞吐量提升: 92% -> 96%
对比CMS:
优势:
1. 可预测的停顿时间
2. 没有内存碎片(整理算法)
3. 空间整合效率高
4. 适合大堆(6GB+)
劣势:
1. 内存占用高(RSet等额外结构)
2. 小堆(<4GB)性能不如CMS
3. 程序运行时额外负载(维护RSet)
适用场景:
- 大内存应用(6GB以上)
- 需要可预测停顿时间
- 替代CMS的首选方案
- JDK9+的默认收集器
2.6 ZGC收集器(JDK11+)
设计目标:
- 停顿时间不超过10ms
- 支持TB级别堆
- 对吞吐量影响<15%
核心技术 - 着色指针(Colored Pointer):
64位对象引用:
+--------+--------+--------+--------+
| 未使用 | 标记位 | 重映射位 | 对象地址 |
| 16bit | 4bit | 1bit | 42bit |
+--------+--------+--------+--------+
支持16TB堆空间(2^42 = 4TB, Linux支持4倍)
标记位用途:
- Marked0: 第一次标记
- Marked1: 第二次标记
- Remapped: 是否已重映射
- Finalizable: 是否只被finalizer引用
核心技术 - 读屏障(Load Barrier):
java
// 每次从堆中读取对象引用时插入读屏障
Object obj = object.field;
// 转换为:
Object obj = barrier(object.field);
// 读屏障作用:
// 1. 检查对象是否在重定位集合中
// 2. 如果是,返回新地址
// 3. 更新引用指向新地址
工作流程:
1. 并发标记(Concurrent Mark)
与应用线程并发执行
使用着色指针标记存活对象
2. 并发预备重分配(Concurrent Prepare for Relocate)
统计需要回收的Region
3. 并发重分配(Concurrent Relocate)
复制存活对象到新Region
通过读屏障处理并发访问
4. 并发重映射(Concurrent Remap)
修正所有指向旧对象的引用
可与下一次标记阶段合并
参数配置:
bash
-XX:+UseZGC # 启用ZGC
-Xmx16g # 最大堆16GB
-XX:ConcGCThreads=4 # 并发GC线程数
-XX:ZCollectionInterval=120 # GC间隔120秒
-XX:ZAllocationSpikeTolerance=2 # 分配峰值容忍度
性能数据:
测试环境: 128GB堆,40核CPU
ZGC:
- 停顿时间: 1-3ms (99.9分位)
- 吞吐量: 92-95%
G1:
- 停顿时间: 50-200ms
- 吞吐量: 95-98%
CMS:
- 停顿时间: 100-500ms
- 吞吐量: 93-96%
适用场景:
- 超大堆内存应用(16GB+)
- 对延迟极度敏感(如金融交易)
- 实时系统
2.7 Shenandoah GC(JDK12+)
设计理念: 与ZGC类似,但实现不同
核心技术 - 转发指针(Forwarding Pointer):
java
// 在对象头中添加转发指针
Object Header:
+----------------+------------------+
| Mark Word | Forwarding Ptr |
| (已有) | (新增) |
+----------------+------------------+
// 并发移动时:
// 1. 在对象头设置转发指针
// 2. 读写屏障检查转发指针
// 3. 重定向到新位置
核心技术 - 读写屏障:
java
// ZGC只有读屏障
// Shenandoah有读写屏障
// 写屏障示例:
object.field = value;
// 转换为:
barrier_write(object, field, value);
// 作用: 保证并发移动时的正确性
参数配置:
bash
-XX:+UseShenandoahGC # 启用Shenandoah
-Xmx16g
-XX:ShenandoahGCHeuristics=adaptive # 自适应启发式
-XX:ShenandoahGCMode=normal # GC模式
对比ZGC:
相同点:
- 目标停顿时间都在10ms以内
- 都支持TB级堆
- 都使用并发整理
不同点:
1. ZGC使用着色指针,Shenandoah使用转发指针
2. ZGC只有读屏障,Shenandoah有读写屏障
3. ZGC需要Linux特定支持,Shenandoah跨平台性更好
三、垃圾收集器选择策略
3. 如何为应用选择合适的垃圾收集器?
决策树:
应用类型?
│
├─ 客户端应用/小内存(<100MB)
│ └─> Serial GC
│
├─ 服务端应用
│ │
│ ├─ 堆内存 < 4GB
│ │ │
│ │ ├─ 吞吐量优先
│ │ │ └─> Parallel GC
│ │ │
│ │ └─ 响应时间优先
│ │ └─> CMS GC (JDK8)
│ │ G1 GC (JDK9+)
│ │
│ ├─ 堆内存 4-32GB
│ │ └─> G1 GC
│ │
│ └─ 堆内存 > 32GB
│ │
│ ├─ 停顿时间要求极低(<10ms)
│ │ └─> ZGC / Shenandoah
│ │
│ └─ 停顿时间要求一般
│ └─> G1 GC
实战选择建议:
bash
# 1. 电商网站(8GB堆,响应时间<100ms)
-XX:+UseG1GC -XX:MaxGCPauseMillis=100
# 2. 批处理系统(16GB堆,吞吐量优先)
-XX:+UseParallelGC -XX:GCTimeRatio=99
# 3. 实时交易系统(64GB堆,延迟<10ms)
-XX:+UseZGC