1. 垃圾回收基础理论
问题:什么是垃圾回收?为什么需要垃圾回收?
详细解答:
垃圾回收定义
垃圾回收(Garbage Collection,GC)是自动内存管理机制,负责识别和回收不再使用的对象所占用的内存空间。
为什么需要GC
手动内存管理的问题:
- C/C++需要手动malloc/free,容易出现内存泄漏
- 野指针问题导致程序崩溃
- 双重释放造成内存损坏
- 开发效率低,需要时刻关注内存释放
GC的优势:
- 自动回收不再使用的对象
- 避免内存泄漏和野指针
- 提高开发效率
- 程序更加健壮可靠
GC的核心问题
三个基本问题:
- 哪些内存需要回收?→ 对象是否存活判断
- 什么时候回收?→ GC触发条件
- 如何回收?→ 垃圾回收算法
2. 对象存活判断算法
问题:如何判断一个对象是否可以被回收?
详细解答:
引用计数法(Reference Counting)
原理:
对象添加引用计数器 引用+1:被引用时 引用-1:引用失效时 引用=0:可回收优点:
- 实现简单
- 判定效率高
- 实时性好(引用计数为0立即回收)
致命缺陷:循环引用
publicclassCircularReference{publicObjectinstance=null;publicstaticvoidmain(String[]args){CircularReferenceobj1=newCircularReference();CircularReferenceobj2=newCircularReference();// 循环引用obj1.instance=obj2;obj2.instance=obj1;obj1=null;obj2=null;// 两个对象互相引用,引用计数永远不为0// 但实际上已经无法访问,造成内存泄漏}}Python的解决方案:
- 使用引用计数 + 标记清除解决循环引用
- JVM没有采用此方案
可达性分析算法(Reachability Analysis)
原理:
从GC Roots对象作为起点向下搜索 搜索路径称为引用链(Reference Chain) 对象到GC Roots没有任何引用链相连 → 不可达 → 可回收GC Roots对象包括:
- 虚拟机栈中引用的对象
publicvoidmethod(){Objectobj=newObject();// obj是GC Root}- 方法区中类静态属性引用的对象
publicclassTest{publicstaticObjectstaticObj=newObject();// staticObj是GC Root}- 方法区中常量引用的对象
publicclassTest{publicstaticfinalObjectCONST_OBJ=newObject();// CONST_OBJ是GC Root}- 本地方法栈中JNI引用的对象
nativevoidnativeMethod();// native方法中引用的对象- JVM内部引用
- 基本类型对应的Class对象
- 异常对象
- 系统类加载器
synchronized持有的对象
JMXBean、JVMTI中注册的回调、本地代码缓存等
可达性分析示例:
GC Roots ├─> Object A ──> Object C ├─> Object B ──> Object D ──> Object E │ └─> Object F Object G <──> Object H (互相引用但不可达) 结论: - A, B, C, D, E, F 可达,存活 - G, H 不可达,可回收引用类型详解
强引用(Strong Reference)
Objectobj=newObject();// 强引用// 只要强引用存在,永远不会被回收// 宁可OOM也不回收软引用(Soft Reference)
SoftReference<byte[]>softRef=newSoftReference<>(newbyte[1024*1024]);// 内存充足:不回收// 内存不足:回收(OOM前)// 应用场景:缓存Map<String,SoftReference<Bitmap>>imageCache=newHashMap<>();弱引用(Weak Reference)
WeakReference<Object>weakRef=newWeakReference<>(newObject());// 无论内存是否充足,GC时一定回收// 生命周期:下次GC前// 应用场景:WeakHashMapWeakHashMap<Key,Value>cache=newWeakHashMap<>();虚引用(Phantom Reference)
ReferenceQueue<Object>queue=newReferenceQueue<>();PhantomReference<Object>phantomRef=newPhantomReference<>(newObject(),queue);// 无法通过虚引用获取对象// 唯一目的:对象被回收时收到系统通知// 应用场景:堆外内存回收(DirectByteBuffer)引用强度比较:
强引用 > 软引用 > 弱引用 > 虚引用对象的自我拯救
finalize()方法机制:
publicclassFinalizeEscapeGC{publicstaticFinalizeEscapeGCSAVE_HOOK=null;publicvoidisAlive(){System.out.println("I am still alive!");}@Overrideprotectedvoidfinalize()throwsThrowable{super.finalize();System.out.println("finalize method executed!");// 自我拯救:重新建立引用FinalizeEscapeGC.SAVE_HOOK=this;}publicstaticvoidmain(String[]args)throwsInterruptedException{SAVE_HOOK=newFinalizeEscapeGC();// 第一次拯救成功SAVE_HOOK=null;System.gc();Thread.sleep(500);// 等待finalize执行if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();// 输出:I am still alive!}else{System.out.println("I am dead!");}// 第二次拯救失败(finalize只执行一次)SAVE_HOOK=null;System.gc();Thread.sleep(500);if(SAVE_HOOK!=null){SAVE_HOOK.isAlive();}else{System.out.println("I am dead!");// 输出这个}}}finalize()的问题:
- 执行时间不确定(低优先级Finalizer线程)
- 性能开销大
- 只执行一次
- 可能导致对象复活
架构师建议:
- 避免使用finalize()
- 使用try-finally或try-with-resources
- JDK 9引入Cleaner机制替代finalize()
3. 垃圾回收算法详解
问题:JVM中有哪些垃圾回收算法?各有什么优缺点?
详细解答:
标记-清除算法(Mark-Sweep)
工作流程:
1. 标记阶段:标记所有需要回收的对象 2. 清除阶段:统一回收被标记的对象示意图:
回收前:[对象A][对象B][对象C][对象D][对象E] 标记后:[对象A][×对象B][对象C][×对象D][对象E] 清除后:[对象A][ ][对象C][ ][对象E]优点:
- 实现简单
- 不需要移动对象
缺点:
- 效率问题:标记和清除效率都不高
- 空间问题:产生大量内存碎片
应用场景:
- CMS收集器的老年代回收
内存碎片问题示例:
// 假设需要分配连续100MB内存byte[]largeArray=newbyte[100*1024*1024];// 虽然总空闲内存>100MB,但没有连续的100MB空间// 导致分配失败,触发Full GC或OOM标记-复制算法(Mark-Copy)
工作流程:
1. 将内存分为两块:From区和To区 2. 使用From区分配对象 3. GC时将From区存活对象复制到To区 4. 清空From区 5. 交换From和To的角色示意图:
From区:[A][B][C][D][E] To区:[空] ↓ GC(B、D为垃圾) From区:[空] To区:[A][C][E]HotSpot的Eden + Survivor实现:
新生代分配:Eden:Survivor0:Survivor1 = 8:1:1 正常情况: Eden + Survivor0 → Survivor1(存活对象<10%) 极端情况: 存活对象>Survivor容量 → 老年代担保分配优点:
- 实现简单
- 运行高效
- 没有内存碎片
缺点:
- 空间浪费:可用内存缩小为原来的一半
- 存活率高时效率降低(需要复制大量对象)
应用场景:
- 新生代回收(对象存活率低,约10%)
代码示例:
// 新生代对象分配publicclassYoungGenAllocation{privatestaticfinalint_1MB=1024*1024;publicstaticvoidmain(String[]args){byte[]allocation1=newbyte[2*_1MB];// Edenbyte[]allocation2=newbyte[2*_1MB];// Edenbyte[]allocation3=newbyte[2*_1MB];// Eden// Eden空间不足,触发Minor GC// allocation1、2、3晋升到老年代byte[]allocation4=newbyte[4*_1MB];}}标记-整理算法(Mark-Compact)
工作流程:
1. 标记阶段:标记存活对象 2. 整理阶段:让所有存活对象向内存一端移动 3. 清理阶段:清理边界外的内存示意图:
标记后:[A][×B][C][×D][E][×F] 整理后:[A][C][E][ ] ↑存活对象 ↑可分配空间两种实现策略:
1. Move策略(移动存活对象)
// 伪代码for(Objectobj:liveObjects){moveToCompactArea(obj);updateReferences(obj);// 更新所有引用}2. Slide策略(滑动压缩)
// 伪代码// 三次扫描:// 1. 计算新地址// 2. 更新引用// 3. 移动对象优点:
- 没有内存碎片
- 空间利用率高
- 适合老年代(存活率高)
缺点:
- 效率问题:需要移动大量对象并更新引用
- 暂停时间长(Stop The World)
应用场景:
- Serial Old收集器
- Parallel Old收集器
分代收集理论
分代假说(Generational Hypothesis):
弱分代假说:
- 绝大多数对象都是朝生夕灭
- 98%的对象在第一次GC后被回收
强分代假说:
- 熬过多次GC的对象越难消亡
- 长时间存活的对象生命周期会更长
跨代引用假说:
- 跨代引用相对于同代引用占极少数
- 存在互相引用关系的对象倾向于同时生存或消亡
分代设计:
堆内存 ├── 新生代(Young Generation) │ ├── Eden区(80%) │ ├── Survivor0区(10%) │ └── Survivor1区(10%) └── 老年代(Old Generation)回收策略:
Minor GC(新生代GC):
- 触发条件:Eden区满
- 回收算法:复制算法
- 频率:高(秒级)
- 停顿时间:短(毫秒级)
Major GC(老年代GC):
- 触发条件:老年代满或晋升失败
- 回收算法:标记-清除或标记-整理
- 频率:低(分钟-小时级)
- 停顿时间:长(可能达到秒级)
Full GC(全堆GC):
- 触发条件:
- 老年代空间不足
- 元空间不足
- System.gc()调用
- CMS GC出现promotion failed、concurrent mode failure
- 回收范围:新生代+老年代+元空间
- 停顿时间:最长
对象晋升规则:
1.长期存活对象进入老年代-XX:MaxTenuringThreshold=15// 默认15次2.大对象直接进入老年代-XX:PretenureSizeThreshold=1048576// 1MB3.动态年龄判定// Survivor空间中相同年龄所有对象大小总和 > Survivor空间一半// 年龄>=该年龄的对象直接进入老年代4.空间分配担保// Minor GC前检查老年代最大连续空间 > 新生代所有对象总大小// 是:安全执行Minor GC// 否:Full GC架构师实战经验:
分代收集优化要点:
- 根据对象生命周期特征调整新生代大小
- 合理设置晋升阈值避免频繁Full GC
- 大对象使用对象池或直接分配到老年代
- 监控晋升速率评估内存配置合理性
4. 垃圾收集器详解
问题:JVM有哪些垃圾收集器?各有什么特点和适用场景?
详细解答:
收集器总览
新生代收集器: - Serial - ParNew - Parallel Scavenge 老年代收集器: - Serial Old - Parallel Old - CMS 全堆收集器: - G1 - ZGC(JDK 11) - Shenandoah(JDK 12)Serial / Serial Old收集器
特点:
- 单线程收集器
- 收集时必须暂停所有工作线程(Stop The World)
- 简单高效(单线程下没有线程交互开销)
工作流程:
用户线程 → [暂停] → Serial GC → [继续] ↓ 单线程回收参数配置:
-XX:+UseSerialGC# 新生代Serial + 老年代Serial Old适用场景:
- Client模式(桌面应用)
- 单核CPU或内存较小的环境
- 对停顿时间不敏感的应用
ParNew收集器
特点:
- Serial的多线程版本
- 新生代并行,老年代串行
- 与CMS配合使用
工作流程:
用户线程 → [暂停] → ParNew GC(多线程) → [继续]参数配置:
-XX:+UseParNewGC# 使用ParNew-XX:ParallelGCThreads=4# GC线程数(通常=CPU核心数)线程数配置建议:
CPU核心数 <= 8:GC线程数 = CPU核心数 CPU核心数 > 8:GC线程数 = 3 + (5 * CPU核心数 / 8)适用场景:
- 多核CPU环境
- 配合CMS使用
Parallel Scavenge / Parallel Old收集器
特点:
- 吞吐量优先收集器
- 新生代和老年代都是并行回收
- 自适应调节策略(GC Ergonomics)
关键参数:
-XX:+UseParallelGC# 新生代Parallel Scavenge-XX:+UseParallelOldGC# 老年代Parallel Old-XX:MaxGCPauseMillis=200# 最大停顿时间(毫秒)-XX:GCTimeRatio=99# 吞吐量大小(默认99,即1%时间GC)-XX:+UseAdaptiveSizePolicy# 自适应调节策略吞吐量计算:
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间) 例如: 运行100分钟,GC 1分钟 吞吐量 = 100 / (100 + 1) = 99%自适应策略:
// JVM自动调整:-新生代大小-Eden与Survivor比例-晋升老年代对象年龄阈值// 目标:在停顿时间和吞吐量之间找到最优解适用场景:
- 后台计算任务(批处理、科学计算)
- 不需要太多交互的应用
- 对停顿时间不敏感但要求高吞吐量
CMS收集器(Concurrent Mark Sweep)
设计目标:
- 获取最短停顿时间
- 互联网站或B/S系统的服务端
工作流程(四个阶段):
1. 初始标记(Initial Mark)- STW
标记GC Roots直接关联的对象 速度快,停顿时间短2. 并发标记(Concurrent Mark)- 并发
从GC Roots遍历整个对象图 与用户线程并发执行 时间最长但不停顿3. 重新标记(Remark)- STW
修正并发标记期间变动的对象标记记录 使用增量更新算法 停顿时间略长于初始标记4. 并发清除(Concurrent Sweep)- 并发
清除死亡对象 与用户线程并发执行时间线:
用户线程: ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ CMS GC: ║ ▒▒▒▒▒▒ ║ ▒▒▒▒ 初始 并发标记 重新 并发清除 标记 标记 ║ = STW停顿 ▒ = 并发执行参数配置:
-XX:+UseConcMarkSweepGC# 使用CMS-XX:CMSInitiatingOccupancyFraction=70# 触发CMS的老年代占用阈值-XX:+UseCMSInitiatingOccupancyOnly# 只使用设定的阈值-XX:+CMSScavengeBeforeRemark# 重新标记前进行一次Minor GC-XX:+UseCMSCompactAtFullCollection# Full GC时进行碎片整理-XX:CMSFullGCsBeforeCompaction=5# 多少次Full GC后整理一次优点:
- 并发收集
- 低停顿
缺点:
1. CPU资源敏感
默认GC线程数 = (CPU核心数 + 3) / 4 4核CPU:1个GC线程,占用25% CPU 2核CPU:1个GC线程,占用50% CPU(影响严重)2. 无法处理浮动垃圾(Floating Garbage)
// 并发标记阶段产生的新垃圾Objectobj=newObject();// 并发标记开始前存在obj=null;// 并发标记期间变为垃圾// 这部分垃圾要等下次GC才能回收3. 内存碎片问题
使用标记-清除算法 产生大量内存碎片 可能导致提前触发Full GC4. Concurrent Mode Failure
触发原因: - 并发清除期间,老年代空间不足以容纳晋升对象 - 预留空间不足(CMSInitiatingOccupancyFraction设置过高) 后果: - 启用Serial Old收集器进行Full GC - 停顿时间大幅增加 解决方案: - 降低CMSInitiatingOccupancyFraction值 - 增加老年代大小适用场景:
- 重视响应速度的应用
- 互联网网站、B/S系统
- 不能容忍长时间停顿的服务
G1收集器(Garbage First)
设计目标:
- 在延迟可控的情况下获得尽可能高的吞吐量
- 替代CMS收集器
核心概念:Region
堆内存划分为多个大小相等的Region(1-32MB) Region类型: - Eden区 - Survivor区 - Old区 - Humongous区(大对象,>=Region大小的50%)工作流程:
1. 初始标记(Initial Mark)- STW
标记GC Roots直接关联的对象 借用Minor GC的暂停2. 并发标记(Concurrent Mark)- 并发
遍历对象图 使用SATB(Snapshot-At-The-Beginning)算法3. 最终标记(Final Mark)- STW
处理SATB缓冲区4. 筛选回收(Live Data Counting and Evacuation)- STW
根据停顿时间目标选择回收Region 将选中Region的存活对象复制到空Region 回收旧Region空间关键技术:
1. Remembered Set(记忆集)
// 记录Region之间的引用关系// 避免全堆扫描// 每个Region维护一个RSetclassRegion{RememberedSetrset;// 记录哪些Region引用了本Region的对象}// Minor GC时只需扫描:// - Eden区// - Survivor区// - RSet记录的引用Region2. Collection Set(回收集合)
记录要被回收的Region集合 根据停顿时间目标动态选择 优先回收垃圾最多的Region(Garbage First)3. 停顿预测模型
// 基于历史数据预测回收时间// 动态选择回收Region数量预测因素:-每个Region的垃圾占比-历史回收耗时-复制存活对象的耗时参数配置:
-XX:+UseG1GC# 使用G1-XX:MaxGCPauseMillis=200# 最大停顿时间目标-XX:G1HeapRegionSize=16m# Region大小-XX:InitiatingHeapOccupancyPercent=45# 触发并发GC的堆占用阈值-XX:G1NewSizePercent=5# 新生代最小占比-XX:G1MaxNewSizePercent=60# 新生代最大占比-XX:ParallelGCThreads=8# 并行GC线程数-XX:ConcGCThreads=2# 并发GC线程数Mixed GC详解:
触发条件: 1. 并发标记完成 2. 老年代占用达到阈值 回收范围: - 整个新生代 - 部分老年代Region 选择策略: 根据停顿时间目标和垃圾占比选择最值得回收的Region优点:
- 可预测的停顿时间
- 没有内存碎片(复制算法)
- 并行与并发结合
- 分代收集但不需要连续空间
缺点:
- 内存占用高(RSet占堆内存约10%-20%)
- 执行负载高(写屏障维护RSet)
- 小堆(<4G)性能可能不如CMS
适用场景:
- 大堆内存(>4G)
- 需要可预测停顿时间
- 替代CMS的生产环境
ZGC收集器(JDK 11+)
设计目标:
- 停顿时间不超过10ms
- 支持TB级堆内存
- 停顿时间不随堆大小增加而增加
核心技术:
1. 着色指针(Colored Pointer)
64位指针布局: [18位未使用][1位Finalizable][1位Remapped] [1位Marked1][1位Marked0][42位对象地址] 通过指针中的标志位标记对象状态2. 读屏障(Load Barrier)
// 每次从堆中读取对象引用时,检查并修复指针Objectobj=object.field;// 读屏障检查指针状态// 必要时进行重新映射3. 并发整理
使用转发表(Forwarding Table) 实现对象移动的并发参数配置:
-XX:+UseZGC# 使用ZGC-XX:ZCollectionInterval=120# GC间隔(秒)-XX:ZAllocationSpikeTolerance=2# 分配尖峰容忍度适用场景:
- 大内存应用(>100G)
- 要求极低延迟(<10ms)
- JDK 11及以上版本
架构师选择建议:
| 场景 | 推荐收集器 | 理由 |
|---|---|---|
| 小堆(<2G)低延迟 | ParNew+CMS | 成熟稳定 |
| 中大堆(4-64G) | G1 | 可预测停顿 |
| 超大堆(>64G)极低延迟 | ZGC | 停顿时间<10ms |
| 批处理高吞吐量 | Parallel | 吞吐量最高 |