最近在工作中遇到频繁FullGC且YoungGC时间有时特别长的情况,而自己对JVM的垃圾回收也是一知半解,因此需要对JVM做系统的了解,为快速解决工作中的问题,能有效分析GC日志和业务代码,先从G1垃圾回收器开始学习(工作中系统采用的就是G1垃圾回收器)
GC全称Garbage Collection,意为垃圾收集。在系统不停机运行中,应用会不断创建对象,也就是不断的在内存中进行空间分配。但系统内存是一定的,不可能支持无限制的内存分配,因此会针对应用持续运行中不再使用的对象所占用的内存空间进行回收。
常见的垃圾收集算法有以下几种:
标记出存活对象后,将存活的对象复制到一块准备好的空闲内存区域,然后将垃圾回收的区域全部回收掉。
上图以年轻代的回收场景来举例,可以看到,复制算法有两个比较重要的点:对象复制的过程、空间区域的冗余。如果一个区域中存活对象较多的场景不适合采用复制算法来进行垃圾回收。还有就是需要准备额外的空闲区域来接收存活对象的放置,因此该算法也有额外空间的损失。
标记出存活对象后,将死亡的对象直接被回收掉,存活对象不做处理。
上图我们也可以看到,这种处理方式很容易造成内存碎片的问题,例如如图所示,有16M的内存空间,通过标记-清除的回收算法,回收6M的内存空间,但无法分配一个连续的4M大小的对象。
标记出存活对象后,将死亡的对象直接回收掉并将存活对象做一步压缩整理,将其压缩到头部连续的空间内。
这就解决了上面说的内存碎片问题。
Java虚拟机发展至今,共有一下几款垃圾收集器:
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的结果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。摘自《深入理解Java虚拟机》
它主要应用于服务端的垃圾回收场景,提供软实时、低延时、可设定目标,适用于较大的堆内存(对比CMS,《深入理解Java虚拟机》作者是这么说的:在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间)。
它的出现也是为了替换JDK 5中的CMS垃圾回收器。在JDK 9发布时将JDK默认的Parallel Scavenge+Parallel Old组合的垃圾回收器修改为G1收集器,而CMS则是被定义为不推荐使用的垃圾回收器。
G1将Java堆分成若干个大小一致的区域,并且Region的大小可以通过-XX:G1HeapRegionSize=N参数设定,取值范围为1MB ~ 32MB,且应为2的N次幂。下图是G1的内存分布示意图(摘自于引用文章)。
从示意图来看,G1仍然保留年轻代、老年代的概念,但新生代和老年代不再是固定的了,也就是同一块Region有可能是年轻代也有可能会被当做是老年代,并且他们在物理上也不是连续的了。图中还有Humongous区域,这个是用来存储大对象的区域,G1认为只要超过了Region大小的一半就是大对象,会被直接存储到该区域当中,它也属于老年代的一部分。
由于G1将垃圾回收的最小单位定位Region,这样就可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1收集器回去跟踪各个Region里面的垃圾堆积的"价值"(回收所获得的空间大小以及回收所需时间的经验值)大小,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数 -XX:MaxGCPauseMillis
指定,默认值200)来优先处理回收价值收益最大的那些Region,这也是"Garbage First"名称的由来。
前面说了,G1会在逻辑上划分年轻代、老年代,其中年轻代又分为Eden、Survivor空间,但年轻代并不是固定不变的,当年轻代分区占满时,JVM会分配新的内存空间加入到年轻代中,整个年轻代内存会在初始空间-XX:G1NewSizePercent
(默认整堆5%)与最大空间(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis
(默认200ms)、需要扩缩容的大小以-XX:G1MaxNewSizePercent
及分区的已记忆集合(RSet)计算得到。当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义。
既然空间上不连续,那G1是如何回收的呢?总不至于去全堆扫描吧?肯定不是的。这就引入了Collection Set(简称CSet)来表示每次GC暂停时回收的一系列目标Region区域。
在G1中,共有两类CSet,一类为保存年轻代Region的集合,另一类为保存年轻代+老年代的Region的集合。年轻代收集集合则保存年轻代的分区,在GC时会将这些分区中的存活对象移动到空闲分区中,然后将原始分区空间全部回收掉。与年轻代集合不同是,老年代集合会有准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent
(默认10%)设置数量上限来避免长时间的暂停。
其实,G1的收集都是根据CSet进行操作的,年轻代收集与混合收集没有明显的不同,最大的区别在于两种收集的触发条件。
我们现在知道G1是通过CSet进行垃圾回收的,那CSet是如何被构建呢?其实针对年轻代Region来讲,一般会将所有分区(有对象分配的)加入到年轻代的CSet中,而对于老年代就比较复杂了,如果从GC Roots开始一直到叶子对象这一整条引用链都分布在一个Region当中就比较方便,在扫描当前Region时就可以将这条引用链标记出来,那么当跨Region引用时,G1是如何标记的呢?肯定不是全堆扫描,答案就是G1会维护一个全局卡表,他本质上是一个哈希结构,Key为每个Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。每个Region都会维护一个记忆集。
上图是一个关于卡表&记忆集的直观图,比较清晰直接,我在学习这块的时候十分困惑,所以也在这里记录下我的理解,如果有错误还请指正。
G1维护了一个全局卡表,这个卡表(代表整个堆)分成若干个卡,也就是分成若干个连续的物理内存区域。当应用线程修改一个对象引用时,涉及的卡便会被标记为"dirty"。而每个Region又维护一个记忆集,这个记忆集本质上是一个哈希结构,key为别的Region指向自己的指针,Value是一个集合,记录了卡表的索引号。这里解释下:比如对象A指向对象B,那么在B的记忆集中就是记录对象A的指针,并且将A所对应的卡片索引号集合(之所以是一个集合,是因为一个对象可能比较大,它占用了多个卡或者一个Region中有多个对象都引用当前Region的对象,它们设计多个卡的情况)
讲述完记忆卡与卡表的实现,那么他们的数据是如何被更新维护的呢?实际上JVM会在编译时注入一小段代码,用于记录指针变化,object.field =
-XX:G1ConcRefinementGreenZone=N
,Refinement线程开始被激活,开始更新RS-XX:G1ConcRefinementYellowZone=N
,全部Refinement线程开始激活-XX:G1ConcRefinementRedZone=N
,产生STW,应⽤线程也参与排空队列的⼯作备注:对G1的垃圾回收原理是基于Jdk 8版本。
G1首先会先进行若干次young gc,每一次gc后,会将存活对象提供给PLAB(Promotion Local Allocation Buffer)来转移对象到老年代或Survivor区。当老年代堆内存占用达到阈值后(-XX:InitiatingHeapOccupancyPercent,默认45%)会进入到并发标记周期,这个周期G1会先进行一次young gc并且伴随着GC Roots的标记(栈中的对象、静态变量、常量、JNI对象等),随后进行与应用线程并发的进行存活对象标记。并发标记结束后,进行重新标记,这个时候标记一下在并发标记过程中产生对象引用更新(通过原始快照+记忆集快速实现),并发标记周期的最后一个操作就是清除阶段,该阶段是对RSet梳理、整理堆分区以及识别所有空闲分区,并发标记周期后,会产生多次mixed gc情况,并不会一次性收集完,这也是G1的启发式算法,根据用户设定的期望时间来优先回收价值最大的一批Region集合。
通过三色标记法+原始快照,细节我们单拉一节来具体介绍。
通过GC日志来看下,young gc各阶段都是干什么的
2023-09-15T16:07:33.481+0800: 158368.669: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 318767104 bytes, new threshold 15 (max 15)
(to-space exhausted), 0.5207419 secs]
[Parallel Time: 316.3 ms, GC Workers: 13]
[GC Worker Start (ms): Min: 158368670.8, Avg: 158368673.3, Max: 158368674.0, Diff: 3.2]
[Ext Root Scanning (ms): Min: 0.4, Avg: 2.5, Max: 6.1, Diff: 5.7, Sum: 32.4]
[Update RS (ms): Min: 6.5, Avg: 15.8, Max: 23.6, Diff: 17.1, Sum: 204.8]
[Processed Buffers: Min: 24, Avg: 93.5, Max: 213, Diff: 189, Sum: 1216]
[Scan RS (ms): Min: 0.0, Avg: 1.2, Max: 1.7, Diff: 1.7, Sum: 16.1]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 282.4, Avg: 291.0, Max: 300.7, Diff: 18.3, Sum: 3783.0]
[Termination (ms): Min: 0.0, Avg: 3.1, Max: 4.8, Diff: 4.8, Sum: 40.3]
[Termination Attempts: Min: 1, Avg: 1.7, Max: 5, Diff: 4, Sum: 22]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.3]
[GC Worker Total (ms): Min: 313.0, Avg: 313.6, Max: 316.1, Diff: 3.1, Sum: 4077.0]
[GC Worker End (ms): Min: 158368986.9, Avg: 158368986.9, Max: 158368987.0, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.5 ms]
[Other: 204.0 ms]
[Evacuation Failure: 172.1 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 27.2 ms]
[Ref Enq: 0.6 ms]
[Redirty Cards: 1.2 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.5 ms]
[Free CSet: 0.3 ms]
[Eden: 3704.0M(4808.0M)->0.0B(3024.0M) Survivors: 0.0B->0.0B Heap: 11.2G(12.0G)->7850.2M(12.0G)]
Heap after GC invocations=4016 (full 26):
garbage-first heap total 12582912K, used 8038625K [0x00000004e0800000, 0x00000004e1003000, 0x00000007e0800000)
region size 8192K, 0 young (0K), 0 survivors (0K)
Metaspace used 159472K, capacity 162234K, committed 162684K, reserved 1193984K
class space used 16726K, capacity 17320K, committed 17532K, reserved 1048576K
}
[Times: user=1.78 sys=0.04, real=0.52 secs]
young gc整个阶段是Stop The World的,日志中的并行时间其实说的是GC线程并行的工作,应用线程是被挂起的。解释完这个我们再来一块一块说明一下它的各个阶段都做了什么。
这两个阶段我学习比较混淆,没找到有效资料
,先记住这两个阶段就是保证这些引用类型能被回收阶段正确处理并发标记周期日志如下所示:
2023-09-18T14:11:36.891+0800: 11.294: [GC pause (Metadata GC Threshold) (young) (initial-mark)
Desired survivor size 243269632 bytes, new threshold 15 (max 15)
, 0.0451481 secs]
[Parallel Time: 17.9 ms, GC Workers: 13]
[GC Worker Start (ms): Min: 11294.1, Avg: 11301.5, Max: 11302.2, Diff: 8.1]
[Ext Root Scanning (ms): Min: 0.3, Avg: 2.2, Max: 8.1, Diff: 7.8, Sum: 28.0]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Code Root Scanning (ms): Min: 0.0, Avg: 1.6, Max: 6.4, Diff: 6.4, Sum: 20.7]
[Object Copy (ms): Min: 0.5, Avg: 4.7, Max: 9.0, Diff: 8.5, Sum: 60.9]
[Termination (ms): Min: 0.0, Avg: 1.9, Max: 2.3, Diff: 2.3, Sum: 24.6]
[Termination Attempts: Min: 1, Avg: 57.7, Max: 157, Diff: 156, Sum: 750]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
[GC Worker Total (ms): Min: 9.6, Avg: 10.3, Max: 17.7, Diff: 8.1, Sum: 134.4]
[GC Worker End (ms): Min: 11311.8, Avg: 11311.8, Max: 11311.9, Diff: 0.1]
[Code Root Fixup: 0.1 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.2 ms]
[Other: 26.9 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 26.4 ms]
[Ref Enq: 0.1 ms]
[Redirty Cards: 0.1 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.1 ms]
[Eden: 240.0M(3680.0M)->0.0B(3664.0M) Survivors: 0.0B->16.0M Heap: 240.0M(12.0G)->15.0M(12.0G)]
Heap after GC invocations=1 (full 0):
garbage-first heap total 12582912K, used 15341K [0x00000004e0800000, 0x00000004e1003000, 0x00000007e0800000)
region size 8192K, 2 young (16384K), 2 survivors (16384K)
Metaspace used 20508K, capacity 21162K, committed 21248K, reserved 1067008K
class space used 2541K, capacity 2793K, committed 2816K, reserved 1048576K
}
[Times: user=0.18 sys=0.01, real=0.05 secs]
2023-09-18T14:11:36.937+0800: 11.339: [GC concurrent-root-region-scan-start]
2023-09-18T14:11:36.937+0800: 11.339: Total time for which application threads were stopped: 0.0454725 seconds, Stopping threads took: 0.0000165 seconds
2023-09-18T14:11:36.956+0800: 11.359: [GC concurrent-root-region-scan-end, 0.0194080 secs]
2023-09-18T14:11:36.956+0800: 11.359: [GC concurrent-mark-start]
2023-09-18T14:11:36.958+0800: 11.361: [GC concurrent-mark-end, 0.0020355 secs]
2023-09-18T14:11:36.958+0800: 11.361: [GC remark 2023-09-18T14:11:36.958+0800: 11.361: [Finalize Marking, 0.0194624 secs] 2023-09-18T14:11:36.978+0800: 11.381: [GC ref-proc, 0.0199802 secs] 2023-09-18T14:11:36.998+0800: 11.401: [Unloading, 0.0062853 secs], 0.0461724 secs]
[Times: user=0.18 sys=0.01, real=0.05 secs]
2023-09-18T14:11:37.005+0800: 11.407: Total time for which application threads were stopped: 0.0465892 seconds, Stopping threads took: 0.0003143 seconds
2023-09-18T14:11:37.005+0800: 11.407: [GC cleanup 26M->26M(12G), 0.0022687 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]
它是借助了一次young gc进行一次根对象的扫描,有一定的性能优化,对于young gc相关阶段不做赘述,这里记录下其他的阶段:
2023-09-18T14:23:33.159+0800: 727.562: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 243269632 bytes, new threshold 15 (max 15)
- age 1: 7529224 bytes, 7529224 total
(to-space exhausted), 0.5513782 secs]
[Parallel Time: 354.4 ms, GC Workers: 13]
[GC Worker Start (ms): Min: 727563.0, Avg: 727564.7, Max: 727565.7, Diff: 2.6]
[Ext Root Scanning (ms): Min: 0.2, Avg: 1.1, Max: 4.7, Diff: 4.5, Sum: 14.1]
[Update RS (ms): Min: 10.9, Avg: 13.9, Max: 16.2, Diff: 5.3, Sum: 180.2]
[Processed Buffers: Min: 20, Avg: 84.1, Max: 133, Diff: 113, Sum: 1093]
[Scan RS (ms): Min: 1.8, Avg: 4.5, Max: 5.6, Diff: 3.8, Sum: 58.5]
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
[Object Copy (ms): Min: 297.8, Avg: 311.1, Max: 332.0, Diff: 34.3, Sum: 4044.3]
[Termination (ms): Min: 0.0, Avg: 21.4, Max: 33.1, Diff: 33.1, Sum: 278.6]
[Termination Attempts: Min: 1, Avg: 1.6, Max: 9, Diff: 8, Sum: 21]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
[GC Worker Total (ms): Min: 351.0, Avg: 352.0, Max: 353.6, Diff: 2.6, Sum: 4575.9]
[GC Worker End (ms): Min: 727916.6, Avg: 727916.7, Max: 727917.3, Diff: 0.7]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 1.5 ms]
[Other: 195.4 ms]
[Evacuation Failure: 172.2 ms]
[Choose CSet: 0.1 ms]
[Ref Proc: 20.3 ms]
[Ref Enq: 0.1 ms]
[Redirty Cards: 0.5 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.4 ms]
[Free CSet: 0.2 ms]
[Eden: 2232.0M(3672.0M)->0.0B(3680.0M) Survivors: 8192.0K->0.0B Heap: 11.3G(12.0G)->9434.1M(12.0G)]
Heap after GC invocations=38 (full 0):
garbage-first heap total 12582912K, used 9660532K [0x00000004e0800000, 0x00000004e1003000, 0x00000007e0800000)
region size 8192K, 0 young (0K), 0 survivors (0K)
Metaspace used 153978K, capacity 156654K, committed 157000K, reserved 1189888K
class space used 16554K, capacity 17174K, committed 17224K, reserved 1048576K
}
[Times: user=1.49 sys=0.03, real=0.55 secs]
mixed gc过程其实是和年轻代gc差不多的,区别就在于CSet的选择。young gc会选择年轻代所有的Region,mixed gc会根据期望停顿时间来选择最有收益的一批Region集合。这里不在赘述了。
好了,对G1垃圾回收器的学习就暂时到这里了,对其有了大概的印象,包括G1垃圾回收的活动周期(young gc、并发标记周期、多次mixed gc)、RSet、CSet以及卡表等知识有了一定的概念,也学习了如何分析G1的GC日志。上面的内容大多是来源与网上的资料,在一些比较难懂的地方加上自己的理解并绘出直观图和语言注释。
https://pdai.tech/md/java/jvm/java-jvm-gc-g1.html
https://www.infoq.com/articles/G1-one-Garbage-Collector-To-Rule-Them-All/
https://time.geekbang.org/column/intro/100010301
《深入理解Java虚拟机》