jvm垃圾回收器 - G1详解
G1垃圾收集器发展史与工作原理G1Garbage First垃圾优先收集器是JVM垃圾收集技术发展史上的里程碑之作它开创了面向局部收集的设计思路和基于Region的内存布局形式定位为CMS收集器的替代者和继承人。一、发展史1.1 早期设计阶段2004-20072004年Sun公司发布G1回收器的核心论文奠定了理论基础。2005年提出Region化内存布局的设计理念。2006年完成Remember Sets记忆集的初步设计。2007年实现首个原型版本。1.2 实验特性阶段2008-20112009年JDK 6u14版本中G1作为实验性功能首次亮相同年增加了自适应堆调整算法。2010年改进大对象Humongous Objects的处理。2011年在OpenJDK 7中发布第一个正式版本。1.3 成熟发展阶段2012-20162012年JDK 7 Update 4中G1进入生产环境推荐使用阶段正式获得官方支持。2014年增强Mixed GC的性能。2015年优化字符串去重功能。2016年改进SATBSnapshot-At-The-Beginning算法。JDK 8 Update 40G1提供了并发的类卸载支持被Oracle称为全功能的垃圾收集器。1.4 默认收集器阶段2017至今2017年JDK 9中G1取代Parallel Scavenge Parallel Old组合正式成为服务端模式下的默认垃圾收集器同时官方废弃Deprecate了CMS收集器。2018年JDK 10引入了并行Full GC。2019年JDK 12优化了Abort Out Of Memory机制。2021年JDK 16引入分代式G1收集器。从2004年第一篇论文发表到2012年达到商用程度G1走过了将近10年的演进之路。二、⚙️ G1 核心概念这里有两个核心概念需要先了解Region区域G1不再将堆内存划分为固定大小的年轻代和老年代而是切分成许多大小相等的小块1MB到32MB之间这就是Region。CSet (Collection Set回收集合)G1每次回收的Region集合。它会从垃圾最多的Region中挑选出一些组成一个“回收集合”然后集中清理这也是“Garbage First”名字的由来。三、 大白话版G1就像个精明的“垃圾管理员”以前的回收方式麻烦又费时早期的垃圾回收器会把整个仓库翻个底朝天——停下来全部扫一遍有用的搬走没用的扔了。问题是仓库越大扫一次的时间就越长程序就得卡住等它做完用户体验很差。1. G1核心思路把大仓库切成小块每次只清理最脏的几个格子假设你有一个堆满杂物的巨大仓库内存仓库里放着很多箱子对象。有些箱子还在用活对象有些箱子早已废弃垃圾。你的任务是定期清理垃圾腾出空间放新箱子。G1 的做法不是一次清空整个仓库而是先把仓库平均划分成几百上千个小格子每个格子就是一块内存区域叫 Region。每个格子里可能有些垃圾有些有用箱子。G1 会优先清理垃圾最多的那几个格子因为同样的精力垃圾越多腾出的空间越大性价比最高。并且每次清理只做一小段时间比如 0.2 秒避免让仓库管理员程序等太久。2. 怎么知道哪个格子垃圾最多—— 偷偷做标记G1 会在仓库正常运转的同时派一个小队在后台悄悄巡逻给每个格子里的箱子做标记活箱子画个 ✓死箱子垃圾画个 ✗巡逻队不会影响仓库正常存取货并发标记。巡逻完毕后G1 就清楚知道每个格子的“垃圾密度”然后排出一个清理顺序表垃圾最多的格子排最前面。3. 清理时怎么做—— 把活箱子搬到其他空格子然后直接把原格子清空当 G1 决定清理几个格子比如垃圾最多的三个格子时它会把这个格子里还在用的活箱子挨个搬到另一个全新的空格子里。搬完后整个旧格子直接推平里面所有垃圾和箱子全部清除。原来那个格子又变成全新的空格子可以重复使用。这样做的效果清理后没有内存碎片像搬家后整理得整整齐齐而且只动少数几个格子速度很快。4. 关键难题格子之间互相指着怎么办仓库里的箱子可能会互相“指着”引用。比如格子 A 里的箱子指向格子 B 里的箱子。如果你要清理格子 B但格子 A 指向了 B 里面的一个箱子那你不能把 B 里的那个箱子当成垃圾。G1 的解决办法给每个格子准备一个小本本Remembered Set简称 RSet。小本本上记录“有哪些其他格子的箱子指着我这个格子的箱子”。比如格子 B 的小本本上写着“格子 A 的第 3 号箱子指向我”。这样当 G1 要清理格子 B 时只需翻阅 B 自己的小本本就知道谁还在用我而不需要把整个仓库翻一遍。这是典型的空间换时间策略。5. 不固定划分年轻/老年代 —— 更灵活传统仓库会把区域固定分成两块新生区新放进来的箱子容易变成垃圾。老年代放很久、一直有用的箱子。G1 不固定分区——每个小格子可以临时扮演新生区或老年代的角色根据需要动态变化。这就避免了“新生区满了但老年代还有很多空位却没法用”的尴尬。6. 万一清理速度跟不上垃圾产生速度怎么办如果程序疯狂制造垃圾G1 的小规模清理来不及仓库慢慢被填满就会触发“破罐破摔模式”——整个仓库锁死一个人单线程把所有格子翻一遍暴力清理所有垃圾。这个过程会卡很久叫 Full GC是最后的保险。一句话大白话总结G1 把内存切成很多小格子平时后台偷偷统计每个格子里的垃圾量然后每次只清理垃圾最多的一些格子并控制在很短时间内完成这样程序几乎感觉不到卡顿。四、工作流程我们深入G1的内部把它的工作流程掰开揉碎了讲清楚。G1的回收并不是单一模式而是两种主要活动交替进行一种是专门清理年轻代的Minor GC另一种是并发的全局并发标记之后再配合混合回收来逐步清理老年代。G1之所以能做到可预测的停顿全靠这个精密的流程。整个流程可以用下面这张图来建立全局印象混合回收并发标记周期年轻代回收是否且还有垃圾多的老RegionEden区满年轻代收集STW全部暂停存活对象晋升或复制到Survivor/Old重置Eden年龄1堆占用达到阈值InitiatingHeapOccupancyPercent初始标记STW伴随一次年轻代GC并发标记与应用线程并发最终标记STW处理SATB缓冲区清理STW统计RSet回收空Region混合回收多次STW每次回收部分老年代RegionCSet是否达到目标暂停时间?结束混合回收阶段下面我们按照时间顺序把每一步都展开包括细节和涉及的G1专用数据结构。1. 普通年轻代回收 (Minor GC / Young GC)这是G1最频繁的活动。触发条件当应用程序不断分配对象所有Eden Region被填满时触发。工作步骤全部STW即Stop-The-World确定CSet回收集合G1会选择所有Eden Region所有Survivor Region。注意此时不选任何Old Region。为什么因为年轻代回收必须快速完成而老年代Region很大扫进去会严重超时。根扫描从GC Roots栈、JNI引用、全局变量等出发标记直接可达的对象。RSet处理检查外部老年代Region对当前CSet中对象的引用。这是关键点G1并不扫描全部老年代而是利用每个Region的RSetRemembered Set记录“谁引用了本Region内的对象”。对于CSet中的每个Region扫描它的RSet把那些从老年代指向年轻代对象的引用也作为GC Roots的一部分。对象拷贝/晋升将CSet中存活的对象拷贝到新的Region中如果对象年龄未达到阈值拷贝到新的Survivor Region。如果对象年龄达到阈值或者目标Survivor Region放不下则拷贝到Old Region晋升。同时这些存活对象在新的位置会记录下它们的引用关系并为新Region维护RSet。清理CSet中的原Region原来的Eden/Survivor Region被完全清空变成空白Region放回空闲队列中。更新RSet所有引用指向新地址并更新对应RSet。特点停顿时间可控因为回收的Region数量大致固定所有年轻代Region且拷贝存活对象的工作量与存活数据量成正比。没有老年代扫描全靠RSet记录外部引用。2. 并发标记周期 (Concurrent Marking Cycle)当老年代占用达到一定比例默认堆的45%由-XX:InitiatingHeapOccupancyPercent控制时G1会启动一次并发标记目的是找出老年代中真正可回收的垃圾为后续混合回收做准备。这个周期包含多个阶段其中只有“初始标记”、“最终标记”、“清理”需要STW其他阶段与应用线程并发。2.1 初始标记 (Initial Mark) - STW时机这个阶段其实是伴随一次年轻代GC发生的不单独暂停。工作在年轻代GC的STW阶段额外标记从GC Roots直接可达的老年代对象例如被静态变量引用的老年代对象、被年轻代对象引用的老年代对象等。这些对象作为并发标记的起点。输出为每个Region维护一个TAMSTop at Mark Start指针表示并发标记开始时Region中已分配对象的顶。并发标记期间新分配的对象位于TAMS以上默认存活。2.2 并发标记 (Concurrent Mark) - 并发工作从初始标记的根出发遍历整个对象图标记所有可达的对象。这个过程与应用线程同时运行。SATB (Snapshot-At-The-Beginning)G1采用SATB算法保证正确性。并发标记开始时逻辑上对整个堆做了一个“快照”。后续应用线程修改引用时G1会通过写屏障Write Barrier记录下被覆盖的旧引用放在SATB缓冲区中。这样并发标记线程仍然能根据快照完成所有存活对象的标记。进度并发标记线程会逐步将对象从“灰色”变为“黑色”最终所有可达对象都被标记为存活。2.3 最终标记 (Final Mark / Remark) - STW目标处理并发标记阶段残留的SATB缓冲区以及在此期间因引用变化而漏标记的对象。工作暂停所有应用线程清空所有的SATB缓冲区确保快照中的所有存活对象都被标记完成。这个阶段比CMS的Remark要快很多因为SATB缓冲区内容较少。2.4 清理 (Cleanup) - STW工作统计Region存活度计算每个Region中存活对象的比例。识别完全空闲的Region如果某个Region中没有任何存活对象立即将它回收到空闲列表不需要拷贝任何东西。更新RSet如果发现某些RSet不再需要例如引用来源Region已被回收也会做修剪。准备混合回收根据存活度对所有老年代Region排序选出垃圾最多的那些Region放入一个候选列表供后续混合回收使用。注意此时并不实际回收那些有垃圾的Old Region只是完成了标记和统计为下一步做决策。3. 混合回收 (Mixed GC)在并发标记完成后G1不会再做传统的Full GC而是执行一系列混合回收Mixed GC。混合回收会同时回收一部分年轻代Region 一部分垃圾最多的老年代Region。触发与执行触发清理阶段结束后G1会先发起一次普通的年轻代回收因为Eden可能又满了但这次回收会额外增加一些老年代Region到CSet中从而变成混合回收。CSet构成全部年轻代RegionEdenSurvivor 若干经过挑选的Old Region从候选列表中选按垃圾最多优先。次数混合回收会连续进行多次每次STW。默认情况下直到候选列表中的老年代Region绝大部分被回收完或者达到了暂停时间目标-XX:G1MixedGCCountTarget控制最多混合回收次数默认8次才会结束。混合回收内部的STW步骤与年轻代回收类似但多了对老年代Region的回收根扫描包括年轻代和老年代GC Roots。RSet处理既要处理年轻代RSet也要处理被回收的老年代Region的RSet查看有哪些外部引用指向它们。对象拷贝年轻代存活对象晋升到Survivor或Old。老年代存活对象也会被拷贝到其他空闲的Old Region。为什么因为当前CSet中的老Region要被整体清空所以必须把里面的存活对象移走。这个过程可能导致对象年龄再次增加甚至晋升到其他老区域。清理与更新清空回收的Region更新RSet。混合回收结束后如果候选列表中仍有垃圾较多的Region且堆占用仍然很高会继续下一次混合回收。如果堆占用下降到阈值以下则暂停混合回收回到普通年轻代回收模式。4. 特殊情况Full GC如果并发标记和混合回收来不及释放内存应用程序又继续分配大量对象导致老年代Region占满或者拷贝晋升时找不到空闲RegionG1就会退化为一次单线程的Full GCJDK 10之前是单线程的JDK 10改为并行Full GC但仍然是全局STW停顿很长。Full GC会压缩整理整个堆标记-清除-压缩。如何尽量避免Full GC合理设置-XX:MaxGCPauseMillis不要太激进否则每次回收量太小积压垃圾调大堆大小或者增加-XX:G1HeapRegionSize以减少Region数量。5. 总结一句话串起流程G1大部分时间在做年轻代回收只清EdenSurvivor当老年代垃圾积累到阈值时就插入一次并发标记标记老年代垃圾标记完成后后续的几次混合回收会同时清理年轻代部分老年代逐步消化垃圾如果一切顺利从不触发Full GC。G1的精髓就在于将全堆回收打散成多次、少量、可预测停顿的回收并且每次只选垃圾最多的Region回收——这就是Garbage First名字的由来。五、重要优化与特性演进版本/JDK重要特性JDK 7u4正式商用支持移除实验标识JDK 8u40并发类卸载支持成为全功能垃圾收集器JDK 9成为服务端默认GC废弃CMSJDK 10并行Full GC引入JDK 12OOM机制优化JDK 16分代式G1收集器持续优化字符串去重、RSet维护开销降低、大内存场景持续优化六、调优参数参数作用默认值-XX:UseG1GC启用G1JDK9默认开启—-XX:G1HeapRegionSizen设置Region大小1~32MB2的幂次-XX:MaxGCPauseMillisn最大停顿时间目标200ms-XX:InitiatingHeapOccupancyPercentn触发并发标记的堆占用阈值45%-XX:G1NewSizePercentn新生代初始大小占比5%-XX:G1MaxNewSizePercentn新生代最大大小占比60%-XX:G1MixedGCCountTargetn混合回收总次数目标8-XX:G1HeapWastePercentn可接受堆垃圾占比5%七、总结与选型建议G1的核心优势可预测的低停顿时间用户可配置目标整体标记-整理算法 区域间复制算法从源头避免内存碎片优先回收垃圾最多的Region最大化回收效率面向大堆内存4GB~64GB和服务器多核环境优化选型建议堆内存大于4GB、需要兼顾吞吐量与低延迟的应用→ G1是首选堆内存较小4GB、对停顿不敏感→ 可考虑Parallel GC要求极致低延迟STW 10ms→ 可考虑ZGCJDK 11JDK 9及以上版本→ G1是默认收集器无需额外配置JDK 8→ 需手动添加-XX:UseG1GC启用
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2640405.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!