一、垃圾回收算法基础
1. 请详细说明主流的垃圾回收算法及其优缺点
问题分析角度:
- 考察对GC算法理论基础的掌握
- 考察算法适用场景的判断能力
- 考察算法演进过程的理解
1.1 标记-清除算法(Mark-Sweep)
算法原理:
- 标记阶段:标记所有需要回收的对象
- 清除阶段:统一回收所有被标记的对象
// 伪代码示例voidmarkSweep(){// 标记阶段for(Objectroot:gcRoots){mark(root);}// 清除阶段for(Objectobj:heap){if(!isMarked(obj)){free(obj);}}}voidmark(Objectobj){if(obj==null||isMarked(obj))return;setMarked(obj);for(Objectref:obj.getReferences()){mark(ref);}}优点:
- 实现简单
- 不需要移动对象
缺点:
- 效率问题:标记和清除效率都不高,需要扫描整个堆
- 空间问题:产生大量内存碎片,可能导致大对象无法分配
回收前: [A][B][C][D][E][F] 回收后: [A][ ][C][ ][E][ ] // 产生碎片应用场景:CMS收集器的老年代回收
1.2 标记-复制算法(Mark-Copy)
算法原理:
- 将内存分为大小相等的两块
- 使用其中一块,满了就将存活对象复制到另一块
- 清空已使用的那块内存
// 伪代码示例voidcopyingGC(){Object[]newSpace=allocateNewSpace();intnewIndex=0;// 复制所有存活对象for(Objectroot:gcRoots){newIndex=copy(root,newSpace,newIndex);}// 交换空间swapSpaces();}intcopy(Objectobj,Object[]newSpace,intindex){if(obj==null||obj.forwardingPointer!=null){returnindex;}// 复制对象到新空间newSpace[index]=obj.clone();obj.forwardingPointer=newSpace[index];// 更新引用for(Objectref:obj.getReferences()){index=copy(ref,newSpace,index+1);}returnindex;}优点:
- 实现简单
- 运行高效
- 没有内存碎片
缺点:
- 内存利用率低(只能使用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实际效果:
// 新生代对象存活率通常很低(研究表明<10%)// 使用8:1:1的比例,实际可用内存达到90%-XX:SurvivorRatio=8// Eden/Survivor=8-XX:+UseAdaptiveSizePolicy// 动态调整比例应用场景:新生代回收(Serial、ParNew、Parallel Scavenge、G1的Young GC)
1.3 标记-整理算法(Mark-Compact)
算法原理:
- 标记阶段:标记所有存活对象
- 整理阶段:将所有存活对象移动到一端,清理边界外的内存
// 伪代码示例voidmarkCompact(){// 标记阶段for(Objectroot:gcRoots){mark(root);}// 计算新地址intnewAddress=heapStart;for(Objectobj:heap){if(isMarked(obj)){obj.forwardingAddress=newAddress;newAddress+=obj.size();}}// 更新引用updateReferences();// 移动对象for(Objectobj: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前检查老年代最大可用连续空间 是否大于新生代所有对象总空间实战案例:
publicclassGenerationExample{// 大对象直接进入老年代byte[]bigObject=newbyte[4*1024*1024];// 4MBpublicstaticvoidmain(String[]args){// 短命对象-在新生代回收for(inti=0;i<1000000;i++){Stringtemp=newString("temp"+i);}// 长命对象-晋升到老年代List<String>cache=newArrayList<>();for(inti=0;i<1000;i++){cache.add(newString("cache"+i));}// 触发多次Minor GC后,cache对象会晋升到老年代for(inti=0;i<10;i++){System.gc();}}}二、垃圾收集器详解
2. 请详细对比各种垃圾收集器的特点和适用场景
问题分析角度:
- 考察对各种收集器的深入理解
- 考察收集器选择的实战经验
- 考察新一代收集器的认知
2.1 Serial收集器(串行收集器)
特点:
- 单线程收集
- 新生代使用标记-复制算法
- 老年代使用标记-整理算法
- 收集时必须Stop The World
工作流程: 用户线程: ||||----[STW]----|||| Serial GC: [GC]参数配置:
-XX:+UseSerialGC# 启用Serial收集器-XX:+UseSerialGC -XX:+UseSerialOldGC# 新生代+老年代都使用Serial优点:
- 简单高效
- 单线程环境下停顿时间最短
- 内存占用小
缺点:
- 多核CPU下性能浪费
- 停顿时间较长
适用场景:
- Client模式下的默认收集器
- 单核CPU或内存受限环境
- 桌面应用程序
2.2 ParNew收集器(并行收集器)
特点:
- Serial的多线程版本
- 新生代并行,老年代串行
- 可与CMS配合使用
工作流程: 用户线程: ||||----[STW]----|||| ParNew GC: [T1][T2][T3]参数配置:
-XX:+UseParNewGC# 启用ParNew-XX:ParallelGCThreads=4# 设置并行线程数(默认=CPU核数)性能对比:
// 测试场景: 4核CPU,2GB堆// Serial: 停顿200ms// ParNew: 停顿80ms (提升60%)// 但在单核环境下,ParNew可能慢于Serial// 因为线程切换开销适用场景:
- 多核CPU环境
- 配合CMS使用(JDK9前唯一选择)
2.3 Parallel Scavenge收集器(吞吐量优先)
特点:
- 关注吞吐量而非停顿时间
- 提供自适应调节策略
- 新生代收集器
核心概念:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间) 例如: 程序运行100分钟,GC 1分钟 吞吐量 = 99 / 100 = 99%参数配置:
-XX:+UseParallelGC# 启用Parallel Scavenge-XX:MaxGCPauseMillis=100# 最大停顿时间100ms-XX:GCTimeRatio=99# 吞吐量目标99%-XX:+UseAdaptiveSizePolicy# 自适应调节(默认开启)# 自适应调节会自动调整:# -Xmn(新生代大小)# -XX:SurvivorRatio(Eden/Survivor比例)# -XX:PretenureSizeThreshold(大对象阈值)吞吐量 vs 停顿时间:
高吞吐量场景: - 后台计算任务 - 批处理作业 - 科学计算 低停顿时间场景: - Web应用 - 交互式应用 - SLA要求高的服务实战案例:
// 场景: 大数据批处理// 需求: 最大化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线程: [清除未标记对象] 与用户线程并发执行参数配置:
-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资源敏感
// 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)
// 并发清除阶段产生的新垃圾无法回收// 需要预留空间给用户线程// 如果预留空间不足,触发"Concurrent Mode Failure"// 启用后备方案Serial Old(Full GC),停顿时间很长// 优化:-XX:CMSInitiatingOccupancyFraction=70// 降低触发阈值- 内存碎片
// 使用标记-清除算法,产生大量碎片// 可能导致大对象无法分配,触发Full GC// 解决方案:-XX:+UseCMSCompactAtFullCollection// Full GC时整理碎片-XX:CMSFullGCsBeforeCompaction=5// 5次Full GC后整理实战案例:
# 电商网站配置(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(记忆集):
// 问题: 如何避免扫描整个堆来确定对象存活?// 解决: 每个Region维护一个RSet,记录外部指向本Region的引用RegionA的RSet:-RegionB的对象引用A中的对象-RegionD的对象引用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参数配置:
-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详解:
// 当老年代占用达到阈值,触发Mixed GC// Mixed GC会回收部分老年代Region// G1根据价值优先选择回收的Region:// 价值 = 回收收益 / 回收时间回收收益=Region中垃圾对象占用的空间 回收时间=复制存活对象所需时间// 示例:RegionA:90%垃圾,需要10msRegionB:50%垃圾,需要20msRegionA价值=90%/10ms=9RegionB价值=50%/20ms=2.5// 优先回收Region A实战调优案例:
# 场景: 4GB堆,要求停顿时间<100ms# 初始配置-Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100# 问题: Young GC频繁,停顿时间超标# 分析GC日志:[GC pause(G1 Evacuation Pause)(young),0.15secs]# 新生代太小,频繁触发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):
// 每次从堆中读取对象引用时插入读屏障Objectobj=object.field;// 转换为:Objectobj=barrier(object.field);// 读屏障作用:// 1. 检查对象是否在重定位集合中// 2. 如果是,返回新地址// 3. 更新引用指向新地址工作流程:
1. 并发标记(Concurrent Mark) 与应用线程并发执行 使用着色指针标记存活对象 2. 并发预备重分配(Concurrent Prepare for Relocate) 统计需要回收的Region 3. 并发重分配(Concurrent Relocate) 复制存活对象到新Region 通过读屏障处理并发访问 4. 并发重映射(Concurrent Remap) 修正所有指向旧对象的引用 可与下一次标记阶段合并参数配置:
-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):
// 在对象头中添加转发指针ObjectHeader:+----------------+------------------+|MarkWord|ForwardingPtr||(已有)|(新增)|+----------------+------------------+// 并发移动时:// 1. 在对象头设置转发指针// 2. 读写屏障检查转发指针// 3. 重定向到新位置核心技术 - 读写屏障:
// ZGC只有读屏障// Shenandoah有读写屏障// 写屏障示例:object.field=value;// 转换为:barrier_write(object,field,value);// 作用: 保证并发移动时的正确性参数配置:
-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实战选择建议:
# 1. 电商网站(8GB堆,响应时间<100ms)-XX:+UseG1GC -XX:MaxGCPauseMillis=100# 2. 批处理系统(16GB堆,吞吐量优先)-XX:+UseParallelGC -XX:GCTimeRatio=99# 3. 实时交易系统(64GB堆,延迟<10ms)-XX:+UseZGC