垃圾收集技术详解笔记
1. 分代收集理论
当前虚拟机的垃圾收集采用分代收集算法,根据对象存活周期将内存分为不同代区,以优化回收效率。
- 核心分区:
- 新生代(Young Generation):对象存活周期短,约99%对象在每次回收时死亡。
- 老年代(Old Generation):对象存活率高,无额外分配担保空间。
- 算法选择:
- 新生代:适用复制算法(效率高,只需复制少量存活对象)。
- 老年代:适用标记-清除或标记-整理算法(避免复制开销,但速度慢10倍以上)。
2. 垃圾收集算法详解
2.1 标记-复制算法
解决效率问题,将内存分为大小相等的两块。
- 工作流程:
- 使用其中一块内存。
- 当内存耗尽,将存活对象复制到另一块。
- 清理原内存块。
- 内存变化示例:
- 整理前:碎片化状态。
- 整理后:存活对象集中到另一块,内存连续。
- 内存区域类型:
- 可用内存、可回收内存、存活对象、保留内存。
2.2 标记-清除算法
最基础算法,分“标记”和“清除”两阶段。
- 工作流程:
- 标记存活对象(或需回收对象)。
- 清除未标记对象。
- 缺点:
- 效率问题:标记大量对象时性能低。
- 空间问题:产生内存碎片(不连续空间)。
- 内存变化示例:
- 整理前:碎片化状态。
- 整理后:碎片化更严重,仅区分可用内存、可回收内存、存活对象。
2.3 标记-整理算法
专为老年代设计,标记后移动对象以消除碎片。
- 工作流程:
- 标记存活对象。
- 将所有存活对象向一端移动。
- 清理边界外内存。
- 内存变化示例:
- 回收前:碎片化状态。
- 回收后:对象紧凑排列,区分存活对象、可回收内存、未使用内存。
3. 垃圾收集器实现
收集器是算法的具体实现,需根据场景选择。无“万能”收集器。
3.1 Serial收集器(-XX:+UseSerialGC, -XX:+UseSerialOldGC)
- 特点:
- 单线程收集器,工作时暂停所有线程(Stop The World)。
- 简单高效(无线程交互开销)。
- 算法:
- 新生代:复制算法。
- 老年代:标记-整理算法。
- 适用场景:客户端模式或小内存应用。
3.2 Parallel Scavenge收集器(-XX:+UseParallelGC, -XX:+UseParallelOldGC)
- 特点:
- Serial的多线程版本(默认线程数 = CPU核数)。
- 关注吞吐量(CPU运行用户代码时间占比)。
- 算法:
- 新生代:复制算法。
- 老年代:标记-整理算法。
- Parallel Old收集器:
- Parallel Scavenge的老年代版本,多线程 + 标记-整理算法。
- JDK8默认收集器,适合高吞吐场景。
3.3 ParNew收集器(-XX:+UseParNewGC)
- 特点:
- 类似Parallel,但专为与CMS收集器配合设计。
- 新生代使用复制算法。
- 适用场景:Server模式下的首选(与CMS兼容)。
3.4 CMS收集器(-XX:+UseConcMarkSweepGC)
- 特点:
- 并发收集器(最短停顿时间),用户线程与GC线程并行。
- 基于标记-清除算法。
- 工作流程:
- 初始标记(STW):标记GC Roots直接引用对象。
- 并发标记:遍历对象图(无停顿)。
- 重新标记(STW):修正并发标记变动(用增量更新算法)。
- 并发清理:清除未标记对象。
- 并发重置:重置标记数据。
- 缺点:
- CPU敏感(与业务线程抢资源)。
- 浮动垃圾(并发阶段新垃圾需下次回收)。
- 内存碎片(需定期整理)。
- 不确定性(可能触发Concurrent Mode Failure,降级为Serial Old)。
- 关键参数:
-XX:+UseConcMarkSweepGC # 启用CMS -XX:ConcGCThreads # 并发GC线程数 -XX:+UseCMSCompactAtFullCollection # FullGC后整理碎片 -XX:CMSFullGCsBeforeCompaction # 多少次FullGC后整理一次(默认0) -XX:CMSInitiatingOccupancyFraction # 老年代使用比例触发FullGC(默认92%) -XX:+CMSScavengeBeforeRemark # CMS前启动Minor GC
4. 亿级流量电商系统JVM优化案例
针对订单系统(8G内存,分配4G给JVM)。
- 优化目标:减少Full GC(避免对象过早进入老年代)。
- 参数配置:
-Xms3072M -Xmx3072M -Xmn2048M # 堆大小(新生代2G) -Xss1M # 线程栈大小 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M # 元空间 -XX:SurvivorRatio=8 # Eden与Survivor比例 -XX:MaxTenuringThreshold=5 # 对象年龄阈值(从15改为5) -XX:PretenureSizeThreshold=1M # 直接进入老年代对象大小阈值 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC # 新生代ParNew + 老年代CMS -XX:CMSInitiatingOccupancyFraction=92 # 老年代92%触发FullGC -XX:+UseCMSCompactAtFullCollection # FullGC后整理碎片 -XX:CMSFullGCsBeforeCompaction=3 # 每3次FullGC整理一次
- 优化原理:
- 增大新生代(-Xmn2048M),减少对象动态年龄判断导致的过早晋升。
- 降低对象年龄阈值(-XX:MaxTenuringThreshold=5),确保短期对象在Minor GC回收。
- CMS默认参数适合高峰后Full GC(几小时一次)。
5. 垃圾收集底层算法
5.1 三色标记算法
解决并发标记中的漏标问题,将对象分为三色:
- 黑色:已扫描完(安全存活)。
- 灰色:已扫描,但引用未全扫描。
- 白色:未扫描(不可达对象)。
- 问题与解决:
- 多标(浮动垃圾):并发阶段局部变量销毁导致本应回收的对象未被回收(下一轮GC处理)。
- 漏标:通过读写屏障解决:
- 增量更新(CMS使用):黑色对象插入新引用时记录,重新扫描。
- 原始快照(SATB, G1使用):灰色对象删除引用时记录,重新扫描。
- 读写屏障实现:
// 写屏障示例(增量更新) void oop_field_store(oop* field, oop new_value) {pre_write_barrier(field); // 写前操作(记录旧值)*field = new_value;post_write_barrier(field, new_value); // 写后操作(记录新值) }
5.2 记忆集与卡表
解决跨代引用问题(如新生代引用老年代)。
- 记忆集(Remember Set):记录跨代指针集合。
- 卡表实现:字节数组(CARD_TABLE[]),每卡页512字节。
- 卡页变脏(=1):有跨代指针时更新。
- 维护:通过写屏障自动更新卡表状态。
6. 总结
- 分代收集是JVM垃圾回收核心,新生代和老年代需匹配不同算法。
- 收集器选择需权衡吞吐量(Parallel Scavenge)和停顿时间(CMS)。
- 优化案例显示:合理配置新生代大小、对象年龄阈值及CMS参数可显著减少Full GC。
- 底层算法(三色标记、读写屏障)确保并发标记的正确性,避免漏标。