参考《周志明.深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)(华章原创精品)(Kindle位置1870).北京华章图文信息有限公司.Kindle版本.》
https://blog.csdn.net/baidu_38083619/article/details/105752830
https://blog.csdn.net/lovejj1994/article/details/109620239
https://zhuanlan.zhihu.com/p/181305087
https://juejin.cn/post/7001406102621388831
尽管可达性分析中耗时最长的查找引用链的过程已经可以做到和用户线程一起并发了,但是根节点的枚举还是必须在一个能保障一致性的快照中才能进行,即根节点枚举阶段,整个执行子系统看起来被冻结在某个时间点上一样。这是导致垃圾收集过程中必须停顿所有用户线程的其中一个重要原因,虽然时间可控,但是这个停顿时不可避免的。
HotSpot在类加载和即时编译阶段都会将对象内各个数据类型的位置都计算出来,在根节点枚举前可以直接利用这些信息生成OopMap这么一个数据结构,直接通过OopMap获取根节点信息。
因为很多指令都会导致引用关系变化,同时也会导致OopMap变化,所以不能在完成每条指令后都生成OopMap,这样开销太大,所以HotSpot只在安全点生成OopMap。
安全点一般在方法调用、循环跳转、异常调转等指令序列复用(具有让程序长时间执行的特征)处选取。
多线程安全点暂停的方案有抢占式中断和主动式中断。目前虚拟机一般都采用主动式中断方式。即在需要中断线程的时候不直接对线程操作,而是简单设置一个标志位,各线程不断主动轮询这个标志位,一旦标志位为真,各线程就在自己最近的安全点上主动中断挂起,一般是到达某个安全点开始轮询标志位。
线程在处于sleep或者阻塞状态时无法响应虚拟机中断,此时如果等线程自己走到安全点将花费大量的时间,这个不太现实,所以引入安全区域的概念。安全区域☞能确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中的任意地方开始垃圾回收都是安全的。可以将安全区域当成安全点的延展。如果线程进入安全区域且发生了GCRoots枚举,需要等待枚举完成后再离开安全区域。
新生代和老年代可能存在跨代引用(只考虑老年代引用新生代的情况),涉及到部分区域收集的时候(G1、ZGC等)也会面临跨区引用。但是我们不能将引用方所在的整个内存区域如老年代整个扫描一遍,这样成本太高,所以可以维护一个数据结构记录引用了目标垃圾收集区域对象的指针,这个数据结构就是记忆集。但是如果到每一个引用指针所在的具体位置成本太高,所以推出了卡表来实现记忆集,将精确到具体指针位置变成精确到一个内存区域,该区域存在跨代指针。如果一个区域内存在跨代指针,就将这个区域内所有的对象都加入GCRoots。
可达性分析过程中查找引用链耗时最长,所以需要此部分和用户线程实现并发进行。如果在并发进行过程中,某几个需要清理的垃圾因为用户线程并发操作导致遗漏并不会有太大的影响,但是如果有几个对象本不该清理却因为用户线程的并发操作导致被清理则会产生致命影响。
相关论文证明了导致有用有对象被清理的两个条件,后续提出了两个解决方案分别用于破坏这两个条件,分别为增量更新和原始快照。CMS是基于增量更新实现并发的,而G1、Shenandoah则是用原始快照来实现的。
下图为经典垃圾收集器的关系图,存在连线的是可以配合使用的垃圾收集器,其中有两个配合在jdk9的时候被禁止。
使用一个收集器、一个线程收集垃圾,同时必须暂停其他所有工作线程,直到收集结束。采用标记-复制算法。
最悠久的收集器,运行在客户端模式下的默认新生代收集器,简单高效(单核环境中基本最强),适用于资源受限的环境,适合客户端或者部分微服务应用,新生代在200MB以内的场景。一般停顿在100毫秒以内。
ParNew收集器是Serial收集器多线程版本,控制参数、收集算法等一致。jdk9之后只能和CMS收集器配合使用。
多线程并行垃圾收集器,但是Parallel Scavenge关注吞吐量(CMS等关注停顿或者说延迟),被称为“吞吐量优先收集器”,吞吐量=(运行用户代码时间)/(运行用户代码时间+运行垃圾收集时间),主要适用与后台计算,交互较少的分析任务。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
XX:+UseAdaptiveSizePolicy开启后,只需要设置-XX:MaxGCPauseMillis或者-XX:GCTimeRatio参数后即可由收集器自动设置新生代大小、代数等细节参数。
Serial收集器的老年代版本,单线程收集器,标记-整理算法,主要用于客户端模式下的HotSpot虚拟机使用,同时作为CMS发生失败的后备预案。Parallel Scavenge收集器中包含老年代收集器,代码和Serial Old收集器一样,所以可以视为两者搭配使用。
Parallel Scavenge收集器的老年代版本,多线程并发收集,标记-整理算法。关注吞吐量,一般和Parallel Scavenge搭配,用于多核处理器且注重吞吐量的场景。
注重减少停顿(延迟),提供较高的响应速度,提高交互体验。步骤为初始标记-并发标记-重新标记-并发清除。
其中并发标记和并发清除是和用户线程并发进行的,而初始标记和重新标记的时间极短,极大的降低了停顿时间。重新标记采用增量更新方式。
CMS是HotSpot追求低停顿的的第一次尝试,但是还有以下几个问题:
1)资源敏感,处理器核数超过4个时,垃圾回收占用不超过25%的资源
2)并发收集时用户线程会产生浮动垃圾,需要预留一定空间存放,需要设定一个阈值提前开始垃圾收集,阈值太低造成回收频率频繁,阈值太高则预留空间不足放不下浮动垃圾,此时造成并发失败,使用Serial Old收集器暂停所有其他线程进行老年代收集,停顿时间很久;
3)CMS处理器基于标记-清除算法,当碎片化程度高时使用标记-整理,此时停顿较大。
G1整体看上去是标记-整理算法,region间看起来又像是标记-复制算法,这两种算法都不会造成内存空间碎片化,有利于程序长期运行。
2)Mixed GC:当整个堆内存(包括老年代和新生代)被占满一定大小的时候(默认是45%,可以通过-XX:InitiatingHeapOccupancyPercent进行设置),Mixed GC(混合回收)就会被启动。具体检测堆内存使用情况的时机是年轻代回收之后或者Houmongous对象分配之后。Mixed GC主要可以分为两个阶段
全局并发标记(global concurrent marking)
包含以下几个阶段:
初始标记(initial mark,STW):在此阶段对GC Root对象进行标记,初始标记阶段共用了Young GC的暂停,这是因为他们可以复用Root Scan操作。
根分区扫描(Root Region Scanning):在初始标记暂停结束后,年轻代收集也完成的对象复制到 Survivor 的工作,应用线程开始活跃起来。此时为了保证标记算法的正确性,所有新复制到 Survivor 分区的对象,都需要被扫描并标记成根。根分区扫描必须在下一次年轻代垃圾收集启动前完成(并发标记的过程中,可能会被若干次年轻代垃圾收集打断),因为每次 GC 会产生新的存活对象集合。
并发标记(Concurrent Marking):在整个堆中查找根可达(存活的)对象,收集各个Region的存活对象信息,过程中还会扫描上文中提到的SATB write barrier所记录下的引用。
重新标记(Remark,STW):标记那些在并发标记阶段发生变化的对象,将被回收。
清理垃圾(Cleanup,部分STW):在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,为混合收集周期识别回收收益高(基于释放空间和暂停目标)的老年代分区集合。识别所有空闲分区,即发现无存活对象的分区。该分区可在清除阶段直接回收,无需等待下次收集周期。
拷贝存活对象(Evacuation)
将Region里的活对象拷贝到空Region里去(并行拷贝),然后回收原本的Region的空间。
为了满足停顿预测模型即暂停时间,G1 可能不能一口气将所有的Region都收集掉,因此 G1 可能会产生连续多次的混合收集与应用线程交替执行,每次 STW 的混合收集与年轻代收集过程相类似。由于老年代中的内存分段默认分8次(可以通过-XX:G1MixedGCCountTarget设置)回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。G1 GC 回收了足够的旧区域后(经过多次混合垃圾回收),G1 将恢复执行年轻代垃圾回收,直到下一个标记周期完成。
3)Full GC
转移失败(Evacuation Failure)是指当 G1 无法在堆空间中申请新的分区时,G1 便会触发担保机制,执行一次 STW 式的、单线程的 Full GC。Full GC 会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数 -XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义。
从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
从老年代分区转移存活对象时,无法找到可用的空闲分区
分配巨型对象时在老年代无法找到足够的连续分区
G1 的Full GC算法就是单线程执行的 Serial Old GC,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免Full GC。
调优建议:
微调 G1 GC 时,请记住以下建议:
年轻代大小:避免使用 -Xmn 选项或 -XX:NewRatio 等其他相关选项显式设置年轻代大小。固定年轻代的大小会覆盖暂停时间目标。
暂停时间目标:-XX:MaxGCPauseMillis设定暂停时间目标,每当对垃圾回收进行评估或调优时,都会涉及到延迟与吞吐量的权衡。G1 GC 是增量垃圾回收器,暂停统一,同时应用程序线程的开销也更多。G1 GC 的吞吐量目标是 90% 的应用程序时间和 10%的垃圾回收时间。因此,当您评估 G1 GC 的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示您愿意承受更多的垃圾回收开销,而这会直接影响到吞吐量。当您评估 G1 GC 的延迟时,请设置所需的(软)实时目标,G1 GC 会尽量满足。副作用是,吞吐量可能会受到影响。
掌握混合垃圾回收:当您调优混合垃圾回收时,请尝试以下选项
-XX:InitiatingHeapOccupancyPercent:堆内存比例,当整个堆占用超过某个百分比时,就会触发并发GC周期,这个百分比默认是45%。
-XX:G1MixedGCLiveThresholdPercent 和 -XX:G1HeapWastePercent:G1MixedGCLiveThresholdPercent是Region的存活比例,默认85%,即只有存活对象低于85%的Region才可以被回收。G1HeapWastePercent是允许堆的浪费比例,默认10%,即全局并发标记后统计出所有可回收垃圾比例超过G1HeapWastePercent才会触发回收混合回收。
-XX:G1MixedGCCountTarget 和 -XX:G1OldCSetRegionThresholdPercent:执行混合垃圾回收的目标次数和设置混合垃圾回收期间要回收的最大旧区域数
G1的推荐用例
G1的首要重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为6GB或更大,并且稳定且可预测的暂停时间低于0.5秒。
如果当前具有CMS或ParallelOld垃圾收集器的应用程序具有以下一个或多个特征,则将其切换到G1很有用。
超过50%的Java堆被实时数据占用。
分代中的对象分配率或提升率差异很大
不必要的长时间垃圾收集或压缩暂停(长于0.5到1秒)
对象分配率和提升率:https://blog.csdn.net/zlfprogram/article/details/77365459
内存占用、吞吐量、延迟构成了不可能三角,一个优秀的收集器最多只能达成两项,三个指标中延迟的重要性越发重要,因为硬件规格提升可以提升吞吐量,但是对延迟反而会带来负面效果(堆越大,垃圾收集时间越长),所以延迟被视为收集器最重要的指标。
后续推出了新的收集器Shenandoah和ZGC,在任意堆大小情况下,停顿时间都不超过10毫秒。
jdk15中,ZGC已经被意见投入生产环境了。最大支持4TB的堆。
Serial+Serial Old:单核处理器,轻量化客户端(jdk9-jdk19)
CMS+parNew:6GB以下堆内存,关注延迟,cpu核数最好大于4
Parallel Scavenge+Parallel Old:关注吞吐(jdk7-jdk8)
Parallel Scavenge+Serial Old:鸡肋,不考虑(jdk5-jdk7)
G1:6GB以上的堆内存
ZGC:jdk15及之后,极低延迟
Shenandoah:openjdk12及之后,极低延迟
jdk5-jdk7:Parallel Scavenge+Serial Old
jdk7-jdk8:Parallel Scavenge+Parallel Old
jdk9-jdk19:G1
注意,有时将Serial Old和Parallel Old统称为PS MarkSweep。故有时jdk5-jdk8都显示为PS Scavenge + PS MarkSweep.