
前面我们吃透了 JVM 内存模型、对象创建机制、逃逸分析、内存分配规则所有知识点最终都会汇聚到一个核心核心模块垃圾收集GC。线上服务的GC卡顿、STW停顿、接口超时、FullGC频繁、OOM内存溢出全部源于对 GC 底层算法、收集器特性、底层三色标记、卡表屏障机制理解不到位。今天这篇文章我们系统性打通 JVM 垃圾收集全套底层体系分代收集理论、三大基础GC算法、四大经典收集器、三色标记底层实现、写屏障/读屏障/记忆集与卡表、亿级流量电商 ParNewCMS 生产调优实战。全篇层层递进、从理论到底层、从原理到生产落地看完彻底掌握 JVM GC 核心搞定面试高阶提问与线上性能调优。一、JVM 分代收集理论所有GC的基石JVM 不使用统一的垃圾回收算法管理所有内存而是基于对象存活生命周期不同将对象内存分为几块,如年轻代和老年代提出分代收集理论针对性使用不同算法回收实现性能最大化。1.1 分代收集三大核心假说弱分代假说绝大多数对象都是朝生夕灭、短期存活对象。强分代假说存活越久的对象越难被回收生命周期更长。跨代引用假说跨代引用相对于同代引用极少存在。基于三大假说JVM 将堆内存划分为年轻代 老年代针对性适配三种经典垃圾回收算法。年轻代对象消亡快、存活少 → 使用高效复制算法老年代对象存活久、存活多、内存空间大 → 使用清除/整理算法1.1 标记-复制算法年轻代核心算法1.1.1 核心原理将内存区域划分为大小相等的两块内存同一时间只使用其中一块。垃圾回收时标记当前使用区域中所有存活对象将所有存活对象完整复制到另一块空闲内存区域清空当前整块内存区域完成垃圾回收交换两块内存的角色下次GC轮换执行。1.1.2 优缺点分析优点无内存碎片内存永远规整只需复制存活对象回收效率极高适合存活对象少、垃圾对象多的场景。缺点内存利用率极低永久浪费 50% 内存空间存活对象多的场景复制开销极大、性能暴跌。1.1.3 适用场景HotSpot 年轻代专属算法适配 EdenSurvivor 内存模型通过 8:1:1 比例优化规避50%内存浪费问题。1.2 标记-清除算法最基础算法1.2.1 核心原理分为两个阶段标记阶段 清除阶段标记阶段从GC Roots遍历所有可达对象标记所有存活对象清除阶段遍历整个内存区域回收所有未标记的垃圾对象内存。1.2.2 优缺点分析优点算法逻辑简单、实现难度低无需内存复制、无内存冗余浪费。缺点产生大量内存碎片导致大对象分配失败触发FullGC两次全内存遍历内存越大GC耗时越长。1.2.3 适用场景早期经典算法现代JVM不再单独使用仅作为CMS回收算法的基础底层逻辑。1.3 标记-整理算法老年代核心算法1.3.1 核心原理在标记清除基础上优化分为标记 整理 清除三步标记所有存活对象将所有存活对象向内存一端统一平移整理清空末端全部垃圾内存保证内存连续规整。1.3.2 优缺点分析优点无内存碎片内存空间规整内存利用率100%无空间浪费。缺点需要移动大量存活对象STW停顿时间最长整体性能开销大吞吐量较低。1.3.3 适用场景老年代专属算法适配存活对象多、对象生命周期长、不适合复制的内存场景Serial Old、G1 整理阶段核心算法。二、主流垃圾收集器深度详解算法是理论收集器是落地实现。不同收集器封装了不同GC算法、适配不同业务场景是生产调优的核心。常见的垃圾收集器有以下几种:2.1 Serial 收集器串行收集器2.1.1 简介最古老、最基础的单线程垃圾收集器全程单线程执行GC用户线程全部暂停。SerialOld收集器是Serial收集器的老年代版本,它主要有两大用途一种用途是在JDK1.5以及以前的版本中与ParallelScavenge收集器搭配使用另一种用途是作为CMS收集器的后备方案。2.1.2 底层算法年轻代标记-复制老年代标记-整理Serial Old2.1.3 优缺点优点简单高效与其他收集器的单线程相比、无线程切换开销、内存占用极小、适合客户端、低配置机器。缺点全程STW、单线程回收、高并发场景卡顿严重不适合服务端生产。2.2 Parallel Scavenge 收集器并行吞吐量收集器2.2.1 简介Parallel收集器其实就是Serial收集器的多线程版本除了使用多线程进行垃圾收集外其余行为控制参数、收集算法、回收策略等等和Serial收集器类似。默认的收集线程数跟cpu核数相同当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数但是一般不推荐修改。ParallelOld收集器是ParallelScavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合都可以优先考虑ParallelScavenge收集器和ParallelOld收集器(JDK8默认的年轻代和老年代收集器)。作为JDK8默认年轻代收集器多线程并行回收核心目标是最大化系统吞吐量。吞吐量 运行用户代码时间 / (用户代码时间 GC时间)2.2.2 底层算法年轻代标记-复制算法多线程并行执行。老年代标记-整理算法多线程并行执行。2.2.3 优缺点优点吞吐量极高、后台线程高效回收、适合后台批量任务、非实时业务。缺点不关注停顿时间高峰期STW卡顿明显不适合高并发、低延迟交易系统。2.3 ParNew CMS 收集器低延迟经典组合2.3.1 ParNew 收集器简介Serial收集器的多线程升级版唯一可以配合CMS的年轻代收集器多线程并行回收年轻代。设置参数为:-XX:UseParNewGC2.3.2 算法标记-复制算法多线程并行执行。2.3.3 优缺点优点回收速度快、停顿短、适配低延迟场景、完美兼容CMS。缺点单纯年轻代收集器依赖老年代CMS配合无法独立工作。2.3.4 CMS 收集器简介并发低延迟收集器CMSConcurrent Mark Sweep并发标记清除收集器是一种以获取最短回收停顿时间为目标的收集器主打低延迟、低停顿是互联网高并发项目经典老年代收集器它第一次实现了让垃圾收集线程与用户线程基本上同时工作。2.3.5 CMS 完整回收流程核心重点CMS 主要分为四大阶段两大阶段STW、两大阶段并发最大化减少停顿初始标记STW仅标记GC Roots直接关联对象速度极快短暂停顿。并发标记并发执行并发标记阶段就是从GCRoots的直接关联对象开始遍历整个对象图的过程。用户线程与GC线程同时运行遍历所有存活对象耗时最长、无停顿。因为用户程序继续运行可能会有导致已经标记过的对象状态发生改变。重新标记STW修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题)这个阶段的停顿时间一般会比初始标记阶段的时间稍长远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法(见下面详解)做重新标记。并发清除并发执行开启用户线程同时GC线程开始对未标记的区域做清扫。无STW不阻塞业务线程。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。并发重置重置本次GC过程中的标记数据。2.3.6 底层算法整体基于标记-清除算法无内存整理并发回收。2.3.7 CMS 优缺点优点大部分阶段并发执行用户线程不阻塞STW停顿时间极短接口延迟低、体验好高并发互联网系统首选经典组合。缺点基于清除算法产生大量内存碎片(可通过参数-XX:UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理)并发阶段占用CPU资源降低吞吐量存在并发浮动垃圾(在并发标记和并发清理阶段又产生垃圾)无法回收本轮新产生垃圾内存使用率过高(上一次垃圾回收还没执行完然后垃圾回收又被触发的情况特别是在并发标记和并发清理阶段会出现一边回收系统一边运行也许没回收完就再次触发fullgc)会触发Concurrent Mode Failure退化为Serial Old单线程FullGC。2.3.8 CMS 核心调优参数1. -XX:UseConcMarkSweepGC启用cms 2. -XX:ConcGCThreads并发的GC线程数 3. -XX:UseCMSCompactAtFullCollection FullGC之后做压缩整理减少碎片 4. -XX:CMSFullGCsBeforeCompaction 多少次FullGC之后压缩一次 默认是0 代表每次FullGC后都会压缩一次 5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC默认是92 这是百分比 6. -XX:UseCMSInitiatingOccupancyOnly只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值) 如果不指定 JVM仅在第一次使用设定值 后续则会自动调整 7. -XX:CMSScavengeBeforeRemark在CMS GC前启动一次minor gc 降低CMS GC标记阶段(也会对年轻代一起做标记 如果 minor gc就干掉了很多对垃圾对象 标记阶段就会减少一些标记时间)时的开销 一般CMS的GC耗时 80%都在标记阶段 8. -XX:CMSParallellnitialMarkEnabled表示在初始标记的时候多线程执行 缩短STW 9. -XX:CMSParallelRemarkEnabled在重新标记的时候多线程执行 缩短STW;2.4 亿级流量电商系统 ParNewCMS 实战JVM参数调优2.4.1 业务场景亿级流量电商、秒杀、订单、支付核心服务要求极低GC卡顿、无频繁FullGC、接口响应稳定、高并发不抖动。服务器配置8核16G 生产高配服务器2.4.2 生产最优参数可直接上线# 亿级电商交易系统 ParNewCMS 生产终极参数 -Xms10G -Xmx10G -Xmn4G -Xss1M -XX:MetaspaceSize256M -XX:MaxMetaspaceSize512M # 开启经典低延迟组合 -XX:UseParNewGC -XX:UseConcMarkSweepGC # 老年代75%触发并发GC提前预热 -XX:CMSInitiatingOccupancyFraction75 -XX:UseCMSInitiatingOccupancyOnly # 并行重新标记降低停顿 -XX:CMSParallelRemarkEnabled # 碎片整理策略 -XX:UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction3 # 关闭显式GC、避免业务System.gc抖动 -XX:DisableExplicitGC # 打印GC日志 -XX:PrintGCDetails -XX:PrintGCTimeStamps -Xloggc:/data/logs/jvm/cms-gc.log2.4.3 核心参数解析固定堆10G防止动态扩容缩容避免内存抖动年轻代4G容纳秒杀瞬时海量临时对象大幅降低MinorGC频率75%触发CMS提前并发回收避免内存堆满触发降级FullGC定时碎片整理解决CMS内存碎片问题防止大对象分配失败禁用显式GC杜绝业务代码误写System.gc导致服务卡顿。2.4.4 上线优化效果GC停顿时间控制在10ms以内用户无感知无频繁FullGC、无CMS降级秒杀高峰期接口无超时、无服务抖动长期运行无内存碎片堆积、无OOM。三、垃圾收集底层核心算法实现高阶底层上面的GC算法、收集器全部依赖底层三色标记算法、读写屏障、记忆集、卡表支撑这是解决并发漏标、跨代引用的底层核心也是大厂面试高阶重难点。3.1 三色标记算法并发GC核心原理3.1.1 简介三色标记是JVM并发标记阶段的对象遍历标记算法解决用户线程与GC线程并发执行时的对象存活判定问题解决漏标问题是CMS、G1并发回收的底层基石。3.1.2 三色定义三色标记算法是把Gcroots可达性分析遍历对象过程中遇到的对象 按照“是否访问过”这个条件标记成以下三种颜色白色表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段 所有的对象都是白色的 若在分析结束的阶段仍然是白色的对象即代表不可达。灰色表示对象已经被垃圾收集器访问过 但这个对象上至少存在一个引用还没有被扫描过。黑色表示对象已经被垃圾收集器访问过 且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过 它是安全存活的 如果有其他对象引用指向了黑色对象 无须重新扫描一遍。 黑色对象不可能直接不经过灰色对象指向某个白色对象。3.1.3 完整执行过程初始所有对象为白色从GC Roots开始遍历根对象标记为灰色遍历灰色对象引用子对象由白变灰父对象变黑循环遍历直至无灰色对象最终白色对象即为垃圾对象可回收。3.1.4 优缺点与并发问题优点支持并发标记、无需全程STW、大幅降低GC停顿。核心问题多标-浮动垃圾在并发标记过程中如果由于方法运行结束导致部分局部变量(gcroot)被销毁这个gcroot引用的对象之前又被扫描过(被标记为非垃圾对象)那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性只是需要等到下一轮垃圾回收中才被清除。另外针对并发标记(还有并发清理)开始后产生的新对象通常的做法是直接全部当成黑色本轮不会进行清除。这部分对象期间可能也会变为垃圾这也算是浮动垃圾的一部分。并发运行时会出现对象漏标导致存活对象被误回收。漏标两大条件灰色对象删除白色对象引用黑色对象新增白色对象引用。解决方案通过读写屏障重新标记修正漏标问题。3.2 读写屏障机制解决并发漏标核心漏标会导致被引用的对象被当成垃圾误删除这是严重bug必须解决有两种解决方案增量更新IncrementalUpdate 和原始快照SnapshotAtTheBeginningSATB 。增量更新就是当黑色对象插入新的指向白色对象的引用关系时就将这个新插入的引用记录下来 等并发扫描结束之后再将这些记录过的引用关系中的黑色对象为根 重新扫描一次。 这可以简化理解为黑色对象一旦新插入了指向白色对象的引用之后它就变回灰色对象了。原始快照就是当灰色对象要删除指向白色对象的引用关系时就将这个要删除的引用记录下来 在并发扫描结束之后再将这些记录过的引用关系中的灰色对象为根 重新扫描一次这样就能扫描到白色的对象将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来待下一轮gc的时候重新扫描这个对象也有可能是浮动垃圾)以上无论是对引用关系记录的插入还是删除 虚拟机的记录操作都是通过写屏障实现的。3.2.1 写屏障对象引用赋值、修改时触发的拦截屏障核心作用记录黑色对象新增的白色引用防止并发标记期间漏标存活对象为重新标记阶段提供修正数据源。3.2.2 读屏障对象引用读取访问时触发的拦截屏障主要用于G1、ZGC等新式收集器实时修正对象标记状态保证并发读取准确性。现代追踪式可达性分析的垃圾回收器几乎都借鉴了三色标记的算法思想尽管实现的方式不尽相同比如白色/黑色集合一般都不会出现但是有其他体现颜色的地方、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可以是广度/深度遍历等等。对于读写屏障以Java HotSpot VM为例其并发标记时对漏标的处理方案如下CMS写屏障 增量更新G1 Shenandoah写屏障 SATBZGC读屏障工程实现中读写屏障还有其他功能比如写屏障可以用于记录跨代/区引用的变化读屏障可以用于支持移动对象的并发执行等。功能之外还有性能的考虑所以对于选择哪种每款垃圾回收器都有自己的想法。为什么G1用SATBCMS用增量更新我的理解SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾)因为不需要在重新标记阶段再次深度扫描被删除引用对象而CMS对增量引用的根对象会做深度扫描 G1因为很多对象都位于不同的region CMS就一块老年代区域重新深度扫描对象的话G1的代价会比CMS高所以G1选择SATB不深度扫描对象只是简单标记等到下一轮GC再深度扫描。3.3 记忆集与卡表解决跨代引用核心3.3.1 核心背景分代回收中存在大量老年代引用年轻代的跨代引用若每次MinorGC都全量扫描老年代开销极大、性能极低。为此在年轻代可以引入记录集RememberSet的数据结构记录从非收集区到收集区的指针集合避免把整个老年代加入GCRoots扫描范围。事实上并不只是年轻代、 老年代之间才有跨代引用的问题 所有涉及部分区域收集PartialGC行为的垃圾收集器典型的如G1、ZGC和Shenandoah收集器都会面临相同的问题。3.3.2 记忆集Remembered Set是一种抽象数据结构作用是记录「非年轻代对象指向年轻代对象」的所有跨代引用让MinorGC无需遍历整个老年代只需扫描记忆集即可。3.3.3 卡表Card Tablehotspot使用一种叫做“卡表”(Cardtable)的方式实现记忆集也是目前最常用的一种方式。关于卡表与记忆集的关系 可以类比为Java语言中HashMap与Map的关系。JVM将老年代内存划分为一个个512字节的卡页每个卡页对应一个卡表标记位脏卡卡页内存在跨代引用干净卡无跨代引用无需扫描。一个卡页中可包含多个对象只要有一个对象的字段存在跨代指针其对应的卡表的元素标识就变成1表示该元素变脏否则为0. GC时只要筛选本收集区的卡表中变脏的元素加入GCRoots里。卡表的维护卡表变脏上面已经说了但是需要知道如何让卡表变脏即发生引用字段赋值时如何更新卡表对应的标识为1。Hotspot使用写屏障维护卡表状态。3.3.4 核心作用大幅缩小GC扫描范围以空间换时间极致提升分代回收效率是现代JVM分代GC的底层核心优化。四、全文核心总结分代收集基于三大假说年轻代用复制算法、老年代用清除/整理算法各司其职。三大基础算法复制高效无碎片、清除简单有碎片、整理无碎片但耗时。经典收集器ParNewCMS主打低延迟Parallel主打吞吐量Serial适合低配置场景。CMS四阶段并发回收是互联网亿级流量系统经典方案需重点优化碎片与触发阈值。三色标记是并发GC核心读写屏障解决漏标问题卡表记忆集解决跨代扫描开销。底层算法屏障卡表机制共同支撑JVM高并发、低卡顿的垃圾回收能力。