在学习前篇Java GC算法背景原理与内存池划分后,相信对于GC
的一些基本算法原理有了基本了解,之后 Java GC算法——日志解读与分析(GC参数基础配置分析),相信对基本的GC
日志有了了解,现在在分析下各种GC
垃圾收集器下的GC
日志
文章目录
- 1. 串行GC(Serial GC)
- 1.1 串行垃圾收集器介绍
- 1.2 串行GC日志解读
- 1.2.1 Minor GC日志分析
- 1.2.2 Full GC日志分析
- 2.并行GC(Parallel GC)
- 2.1 并行垃圾收集器介绍
- 2.2 并行GC日志解读
- 2.2.1 Minor GC日志分析
- 2.2.2 Full GC日志分析
- 3. CMS GC
- 3.1 CMS垃圾收集器介绍
- 3.2 CMS GC日志解读
- 3.2.1 Minor GC日志分析
- 3.2.2 Full GC日志分析
- 3.2.2.1 阶段 1: Initial Mark(初始标记) (有STW)
- 3.2.2.2 阶段 2: Concurrent Mark(并发标记)
- 3.2.2.3 阶段 3: Concurrent Preclean(并发预清理)
- 3.2.2.4 阶段 4: Concurrent Abortable Preclean(可取消的并发预清理)
- 3.2.2.5 阶段 5: Final Remark(最终标记) (有STW)
- 3.2.2.6 阶段 6: Concurrent Sweep(并发清除)
- 3.2.2.7 阶段 7: Concurrent Reset(并发重置)
- 4. G1垃圾收集器
- 4.1 G1垃圾收集器介绍
- 4.2 G1 GC常用参数设置
- 4.3 G1日志解读和分析
- 4.3.1 年轻代模式转移暂停(Evacuation Pause)
- 4.3.2 Concurrent Marking(并发标记)
- 4.3.2.1 阶段 1: Initial Mark(初始标记)
- 4.3.2.2 阶段 2: Root Region Scan(Root区扫描)
- 4.3.2.3 阶段 3: Concurrent Mark(并发标记)
- 4.3.2.4 阶段 4: Remark(再次标记)
- 4.3.2.5 阶段 5: Cleanup(清理)
- 4.3.3 转移暂停: 混合模式(Evacuation Pause (mixed))
- 4.3.4 Full GC (Allocation Failure)
- 5. GC选择的经验总结
前文学习了GC
算法的相关概念后,这里我们会讲解JVM
中这些算法的具体实现。大多数JVM
都需要使用两种不同的GC
算法 —— 一种用来清理年轻代,另一种用来清理老年代。我们可以选择JVM
内置的各种算法。如果不通过参数明确指定垃圾收集算法,则会使用相应JDK
版本的默认实现。本文会详细介绍各种算法的实现原理。
1. 串行GC(Serial GC)
1.1 串行垃圾收集器介绍
串行GC
对年轻代使用 mark-copy
(标记-复制)算法,对老年代使用 mark-sweep-compact
(标记- 清除-整理) 算法。 由于是单线程的垃圾收集器,不能进行并行处理,所以对年轻代和老年代的垃圾收集都会触发全线暂停(STW
),停止所有的应用线程。因此这种GC
算法不能充分利用多核CPU
。不管有多少CPU
内核,JVM
在垃圾收集时都只能使用单个核心。
要启用此款收集器,只需要指定一个JVM
启动参数即可,同时对年轻代和老年代生效
-XX:+UseSerialGC
该选项只适合几百MB
堆内存的JVM
,而且是单核CPU
时比较有用。 对于服务器端来说,因为一般是多个CPU
内核,并不推荐使用,除非确实需要限制JVM
所使用的资源。 大多数服务器端应用部署在多核平台上,选择串行GC
就意味着人为地限制了系统资源的使用,会导致资源闲 置,多余的CPU
资源也不能用增加业务处理的吞吐量。
1.2 串行GC日志解读
java -XX:+UseSerialGC -Xms512m -Xmx512m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.demo.log GCLogAnalysis
让我们看看Serial GC
的垃圾收集日志(由于写作的原因,这里是windows
系统,日志开头显示了windows-amd64
)
Java HotSpot(TM) 64-Bit Server VM (25.212-b10) for windows-amd64 JRE (1.8.0_212-b10), built on Apr 1 2019 22:50:23 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16633820k(5238816k free), swap 25449536k(5187744k free)
CommandLine flags: -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
2021-12-16T19:30:03.983+0800: 0.155: [GC (Allocation Failure) 2021-12-16T19:30:03.983+0800: 0.155: [DefNew: 139776K->17472K(157248K), 0.0155099 secs] 139776K->42655K(506816K), 0.0157119 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2021-12-16T19:30:04.016+0800: 0.189: [GC (Allocation Failure) 2021-12-16T19:30:04.016+0800: 0.189: [DefNew: 157211K->17471K(157248K), 0.0195822 secs] 182394K->81289K(506816K), 0.0196621 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
2021-12-16T19:30:04.052+0800: 0.225: [GC (Allocation Failure) 2021-12-16T19:30:04.052+0800: 0.225: [DefNew: 157247K->17470K(157248K), 0.0176595 secs] 221065K->127182K(506816K), 0.0177252 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2021-12-16T19:30:04.089+0800: 0.262: [GC (Allocation Failure) 2021-12-16T19:30:04.089+0800: 0.262: [DefNew: 157246K->17471K(157248K), 0.0180485 secs] 266958K->165146K(506816K), 0.0181250 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
2021-12-16T19:30:04.128+0800: 0.300: [GC (Allocation Failure) 2021-12-16T19:30:04.128+0800: 0.300: [DefNew: 156973K->17471K(157248K), 0.0181455 secs] 304647K->207272K(506816K), 0.0182251 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
2021-12-16T19:30:04.164+0800: 0.336: [GC (Allocation Failure) 2021-12-16T19:30:04.164+0800: 0.336: [DefNew: 157247K->17471K(157248K), 0.0205090 secs] 347048K->249163K(506816K), 0.0206178 secs] [Times: user=0.00 sys=0.01, real=0.02 secs]
2021-12-16T19:30:04.203+0800: 0.375: [GC (Allocation Failure) 2021-12-16T19:30:04.203+0800: 0.375: [DefNew: 157143K->17471K(157248K), 0.0188082 secs] 388834K->295301K(506816K), 0.0188897 secs] [Times: user=0.02 sys=0.02, real=0.02 secs]
2021-12-16T19:30:04.239+0800: 0.411: [GC (Allocation Failure) 2021-12-16T19:30:04.239+0800: 0.412: [DefNew: 157247K->17471K(157248K), 0.0165640 secs] 435077K->336036K(506816K), 0.0166406 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2021-12-16T19:30:04.273+0800: 0.445: [GC (Allocation Failure) 2021-12-16T19:30:04.273+0800: 0.445: [DefNew: 157247K->157247K(157248K), 0.0000144 secs]2021-12-16T19:30:04.273+0800: 0.445: [Tenured: 318564K->268197K(349568K), 0.0343878 secs] 475812K->268197K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0345041 secs] [Times: user=0.03 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.323+0800: 0.495: [GC (Allocation Failure) 2021-12-16T19:30:04.323+0800: 0.495: [DefNew: 139776K->17470K(157248K), 0.0060237 secs] 407973K->313385K(506816K), 0.0061055 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2021-12-16T19:30:04.348+0800: 0.520: [GC (Allocation Failure) 2021-12-16T19:30:04.348+0800: 0.520: [DefNew: 156845K->17470K(157248K), 0.0138671 secs] 452760K->360125K(506816K), 0.0139485 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
2021-12-16T19:30:04.380+0800: 0.552: [GC (Allocation Failure) 2021-12-16T19:30:04.380+0800: 0.552: [DefNew: 157246K->157246K(157248K), 0.0000148 secs]2021-12-16T19:30:04.380+0800: 0.552: [Tenured: 342654K->316653K(349568K), 0.0405361 secs] 499901K->316653K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0406369 secs] [Times: user=0.03 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.437+0800: 0.610: [GC (Allocation Failure) 2021-12-16T19:30:04.437+0800: 0.610: [DefNew: 139776K->139776K(157248K), 0.0000156 secs]2021-12-16T19:30:04.437+0800: 0.610: [Tenured: 316653K->328505K(349568K), 0.0362663 secs] 456429K->328505K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0363715 secs] [Times: user=0.05 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.489+0800: 0.661: [GC (Allocation Failure) 2021-12-16T19:30:04.489+0800: 0.661: [DefNew: 139776K->139776K(157248K), 0.0000696 secs]2021-12-16T19:30:04.489+0800: 0.661: [Tenured: 328505K->316403K(349568K), 0.0373156 secs] 468281K->316403K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0374730 secs] [Times: user=0.03 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.547+0800: 0.719: [GC (Allocation Failure) 2021-12-16T19:30:04.547+0800: 0.719: [DefNew: 139776K->139776K(157248K), 0.0000145 secs]2021-12-16T19:30:04.547+0800: 0.719: [Tenured: 316403K->334486K(349568K), 0.0244239 secs] 456179K->334486K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0245235 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2021-12-16T19:30:04.589+0800: 0.762: [GC (Allocation Failure) 2021-12-16T19:30:04.590+0800: 0.762: [DefNew: 139776K->139776K(157248K), 0.0000183 secs]2021-12-16T19:30:04.590+0800: 0.762: [Tenured: 334486K->345826K(349568K), 0.0350912 secs] 474262K->345826K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0352064 secs] [Times: user=0.03 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.640+0800: 0.812: [GC (Allocation Failure) 2021-12-16T19:30:04.640+0800: 0.812: [DefNew: 139776K->139776K(157248K), 0.0000143 secs]2021-12-16T19:30:04.640+0800: 0.812: [Tenured: 345826K->349080K(349568K), 0.0349528 secs] 485602K->351316K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0350525 secs] [Times: user=0.05 sys=0.00, real=0.03 secs]
2021-12-16T19:30:04.693+0800: 0.865: [Full GC (Allocation Failure) 2021-12-16T19:30:04.693+0800: 0.865: [Tenured: 349296K->343154K(349568K), 0.0397555 secs] 506535K->343154K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0398233 secs] [Times: user=0.03 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.747+0800: 0.919: [GC (Allocation Failure) 2021-12-16T19:30:04.747+0800: 0.919: [DefNew: 139776K->139776K(157248K), 0.0000146 secs]2021-12-16T19:30:04.747+0800: 0.919: [Tenured: 343154K->349368K(349568K), 0.0243795 secs] 482930K->368942K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0244925 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
2021-12-16T19:30:04.789+0800: 0.961: [Full GC (Allocation Failure) 2021-12-16T19:30:04.789+0800: 0.961: [Tenured: 349368K->349326K(349568K), 0.0363575 secs] 506527K->366420K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0364563 secs] [Times: user=0.03 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.842+0800: 1.014: [Full GC (Allocation Failure) 2021-12-16T19:30:04.842+0800: 1.014: [Tenured: 349423K->349253K(349568K), 0.0359097 secs] 506586K->368491K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0359858 secs] [Times: user=0.05 sys=0.00, real=0.04 secs]
2021-12-16T19:30:04.892+0800: 1.064: [Full GC (Allocation Failure) 2021-12-16T19:30:04.892+0800: 1.065: [Tenured: 349565K->349260K(349568K), 0.0407681 secs] 506795K->354364K(506816K), [Metaspace: 2608K->2608K(1056768K)], 0.0408418 secs] [Times: user=0.03 sys=0.00, real=0.04 secs]
Heap
def new generation total 157248K, used 11403K [0x00000000e0000000, 0x00000000eaaa0000, 0x00000000eaaa0000)
eden space 139776K, 8% used [0x00000000e0000000, 0x00000000e0b22e98, 0x00000000e8880000)
from space 17472K, 0% used [0x00000000e8880000, 0x00000000e8880000, 0x00000000e9990000)
to space 17472K, 0% used [0x00000000e9990000, 0x00000000e9990000, 0x00000000eaaa0000)
tenured generation total 349568K, used 349260K [0x00000000eaaa0000, 0x0000000100000000, 0x0000000100000000)
the space 349568K, 99% used [0x00000000eaaa0000, 0x00000000fffb3190, 0x00000000fffb3200, 0x0000000100000000)
Metaspace used 2615K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
日志的第一行是JVM
版本信息,第二行往后到第一个时间戳之间的部分,展示了内存分页、物理内存大小, 命令行参数等信息。
1.2.1 Minor GC日志分析
2021-12-16T19:30:03.983+0800: 0.155: [GC (Allocation Failure) 2021-12-16T19:30:03.983+0800: 0.155:
[DefNew: 139776K->17472K(157248K), 0.0155099 secs] 139776K->42655K(506816K), 0.0157119 secs]
[Times: user=0.02 sys=0.00, real=0.02 secs]
从中可以解读出这些信息:
-
2021-12-16T19:30:03.983+0800: 0.155:
,这是GC
事件开始的时间点。其中+0800
表示当前时区为东八区(北京时间),这只是一个标识,方便我们直观判断GC
发生的时间点。后面的0.155
是GC
事件相对于JVM
启动时间的间隔,单位是秒。 -
GC
– 用来区分Minor GC
还是Full GC
的标志。GC
表明这是一次小型GC(Minor GC)
,即年轻代GC
。Allocation Failure
表示触发GC
的原因。本次GC
事件,是由于对象分配失败,年轻代中没有空间来存放新生成的对象引起的。 -
DefNew
– 表示垃圾收集器的名称。这个名字表示:这是一个年轻代使用的单线程、标记-复制、需要STW
的垃圾收集器。139776K->17472K
表示在垃圾收集之前和之后的年轻代使用量。(157248K)
表示年轻代的总空间大小。进一步分析可知:GC
之后年轻代使用率为11%
-
139776K->42655K
– 表示在垃圾收集之前和之后整个堆内存的使用情况。(506816K)
则表示堆内存可用的总空间大小。进一步分析可知:GC
之后堆内存使用量为8%
-
0.0157119 secs
–GC
事件持续的时间,以秒为单位 -
[Times: user=0.02 sys=0.00, real=0.02 secs]
– 此次GC
事件的持续时间,通过三个部分来衡量:user
部分表示所有GC
线程消耗的CPU
时间;sys
部分表示系统调用和系统等待事件消耗的时间。real
则表示应用程序暂停的时间。
这个暂停时间对大部分系统来说可以接受,但对某些延迟敏感的系统就不太理想了,比如实时的游戏服务、高频交易业务,30ms
暂停是致命的。
GC
前后对比,年轻代的使用量为 139776K->17472K
,减少了 122304K
。 但堆内存的总使用量 139776K->42655K
只下降了 97121K
。 可以算出,从年轻代提升到老年代的对象占用了 122304K - 97121K = 25183K
的内存空间。当然, 另一组数字也能推算出GC
之后老年代的使用量 42655K - 17472K = 25183K
我们关注的主要是两个数据: GC
暂停时间以及GC
之后的内存使用量/使用率
此次GC
事件的示意图如下所示:
1.2.2 Full GC日志分析
2021-12-16T22:53:35.154+0800: 0.439: [GC (Allocation Failure) 2021-12-16T22:53:35.154+0800: 0.439:
[DefNew: 157245K->157245K(157248K), 0.0000141 secs]2021-12-16T22:53:35.154+0800: 0.439:
[Tenured: 294996K->263651K(349568K), 0.0346014 secs] 452242K->263651K(506816K),
[Metaspace: 2608K->2608K(1056768K)], 0.0347088 secs]
[Times: user=0.05 sys=0.00, real=0.03 secs]
从中可以解读出这些信息:
2021-12-16T22:53:35.154+0800: 0.439:
–GC
事件开始的时间。[DefNew: 157245K->157245K(157248K), 0.0000141 secs]
– 前面已经解读过了,因为内存分配失败,发生了一次年轻代GC
。此次GC
同样用的DefNew
收集器。注意:此次垃圾收集消耗了0.0000141
秒,基本上确认本次GC
事件没怎么处理年轻代。Tenured
– 用于清理老年代空间的垃圾收集器名称。Tenured
表明使用的是单线程的STW
垃圾收集器,使用的算法为标记-清除-整理(mark-sweep-compact)
。294996K->263651K(349568K)
表示GC
前后老年代的使用量,以及老年代的空间大小。0.0346014 secs
是清理老年代所花的时间。452242K->263651K(506816K)
– 在GC
前后整个堆内存部分的使用情况,以及可用的堆空间大小。[Metaspace: 2608K->2608K(1056768K)]
–Metaspace
空间的变化情况。可以看出,此次GC
过程中Metaspace
也没有什么变化。[Times: user=0.05 sys=0.00, real=0.03 secs]
–GC
事件的持续时间,分为user
,sys
,real
三个部分。
注意:GC的时间与GC后存活对象的总数量关系最大
进一步分析这些数据,GC
之后老年代的使用率为: 263651K / 349568K = 75%
,这个比例不小了,但也不能就此说出了什么问题,毕竟GC
后内存使用量下降了,还需要后续的观察。
和年轻代GC
相比,比较明显的差别是此次GC
事件清理了老年代。
总结:关于FullGC
,主要关注GC
之后内存使用量是否下降,其次关注暂停时间。简单估算,GC
后老年代使用量为260MB
左右,耗时30ms
。如果内存扩大10
倍,GC
后老年代内存使用量也扩大10
倍,那耗时可能就是300ms
甚至更高,就会系统有很明显的影响了。这也是我们说串行GC
性能弱的一个原因, 服务端一般是不会采用串行GC
的。
此次GC
事件的内存变化情况,可以表示为下面的示意图
年轻代看起来数据几乎没变化,重点看看上下文,其实还有其他的GC
日志记录。
2.并行GC(Parallel GC)
2.1 并行垃圾收集器介绍
并行垃圾收集器这一类组合,在年轻代使用 标记-复制(mark-copy
)算法 ,在老年代使用 标记-清除- 整理(mark-sweep-compact
)算法 。年轻代和老年代的垃圾回收都会触发STW
事件,暂停所有的应用线程来执行垃圾收集。两者在执行标记和复制/整理阶段时都使用多个线程,因此得名Parallel
。通过多个GC
线程并行执行,能使JVM
在多CPU
平台上的GC
时间大幅减少。
通过命令行参数 -XX:ParallelGCThreads=NNN
来指定 GC
线程数, 其默认值为CPU
核心数。 可以通过下面的任意一组命令行参数来指定并行GC
:
-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:+UseParallelGC -XX:+UseParallelOldGC
并行垃圾收集器适用于多核服务器,主要目标是增加吞吐量(也就是降低GC
总体消耗的时间)。为了达成这个目标,会尽量使用尽可能多的CPU
资源:
- 在
GC
事件执行期间,所有CPU
内核都在并行清理垃圾,所以暂停时间相对来说更短; - 在两次
GC
事件中间的间隔期,不会启动GC
线程,所以这段时间内不会消耗任何系统资源;
另一方面,因为此GC
的所有阶段都不能中断,所以并行GC
很容易出现长时间的卡顿(注:长时间卡顿的意思是,并行GC
启动后,一次性完成所有的GC
操作,于是单次暂停的时间会较长。这里说的长时间也很短,一般来说例如minor GC
是毫秒级别,full GC
是几十几百毫秒级别)。如果系统延迟是非常重要的性能指标(主要目标是最低的停顿时间/延迟),而不是整体的吞吐量最大,那么就应该选择其他垃圾收集器组合。
2.2 并行GC日志解读
java -XX:+UseParallelGC -Xms512m -Xmx512m -Xloggc:gc.demo.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps GCLogAnalysis
让我们看看Parallel GC
的垃圾收集日志(由于写作的原因,这里是mac
系统,日志开头显示了bsd-amd64
)
Java HotSpot(TM) 64-Bit Server VM (25.202-b08) for bsd-amd64 JRE (1.8.0_202-b08), built on Dec 15 2018 20:16:16 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 8388608k(17228k free)
/proc/meminfo:
CommandLine flags: -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
2021-12-26T12:29:48.527-0800: 0.287: [GC (Allocation Failure) [PSYoungGen: 131041K->21492K(153088K)] 131041K->49246K(502784K), 0.0152442 secs] [Times: user=0.03 sys=0.05, real=0.02 secs]
2021-12-26T12:29:48.564-0800: 0.324: [GC (Allocation Failure) [PSYoungGen: 152957K->21498K(153088K)] 180712K->93172K(502784K), 0.0203832 secs] [Times: user=0.03 sys=0.07, real=0.02 secs]
2021-12-26T12:29:48.600-0800: 0.361: [GC (Allocation Failure) [PSYoungGen: 153082K->21492K(153088K)] 224756K->131158K(502784K), 0.0145353 secs] [Times: user=0.03 sys=0.04, real=0.01 secs]
2021-12-26T12:29:48.632-0800: 0.392: [GC (Allocation Failure) [PSYoungGen: 153076K->21499K(153088K)] 262742K->171192K(502784K), 0.0131396 secs] [Times: user=0.03 sys=0.04, real=0.01 secs]
2021-12-26T12:29:48.662-0800: 0.422: [GC (Allocation Failure) [PSYoungGen: 153083K->21500K(153088K)] 302776K->213889K(502784K), 0.0130427 secs] [Times: user=0.04 sys=0.05, real=0.01 secs]
2021-12-26T12:29:48.691-0800: 0.452: [GC (Allocation Failure) [PSYoungGen: 153084K->21491K(80384K)] 345473K->257674K(430080K), 0.0217015 secs] [Times: user=0.04 sys=0.03, real=0.02 secs]
2021-12-26T12:29:48.720-0800: 0.480: [GC (Allocation Failure) [PSYoungGen: 80371K->34010K(116736K)] 316554K->276425K(466432K), 0.0044657 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]
2021-12-26T12:29:48.732-0800: 0.492: [GC (Allocation Failure) [PSYoungGen: 92792K->44819K(116736K)] 335207K->292058K(466432K), 0.0047677 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
2021-12-26T12:29:48.744-0800: 0.505: [GC (Allocation Failure) [PSYoungGen: 103238K->53411K(116736K)] 350478K->308994K(466432K), 0.0064440 secs] [Times: user=0.04 sys=0.01, real=0.01 secs]
2021-12-26T12:29:48.759-0800: 0.519: [GC (Allocation Failure) [PSYoungGen: 112291K->35958K(116736K)] 367874K->322789K(466432K), 0.0160100 secs] [Times: user=0.03 sys=0.03, real=0.02 secs]
2021-12-26T12:29:48.783-0800: 0.544: [GC (Allocation Failure) [PSYoungGen: 94838K->23416K(116736K)] 381669K->340530K(466432K), 0.0092515 secs] [Times: user=0.02 sys=0.03, real=0.01 secs]
2021-12-26T12:29:48.793-0800: 0.553: [Full GC (Ergonomics) [PSYoungGen: 23416K->0K(116736K)] [ParOldGen: 317113K->232542K(349696K)] 340530K->232542K(466432K), [Metaspace: 2560K->2560K(1056768K)], 0.0268958 secs] [Times: user=0.17 sys=0.00, real=0.02 secs]
2021-12-26T12:29:48.828-0800: 0.588: [GC (Allocation Failure) [PSYoungGen: 58663K->18278K(116736K)] 291205K->250820K(466432K), 0.0017320 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2021-12-26T12:29:48.838-0800: 0.599: [GC (Allocation Failure) [PSYoungGen: 76934K->20931K(116736K)] 309477K->270511K(466432K), 0.0057888 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]
2021-12-26T12:29:48.853-0800: 0.613: [GC (Allocation Failure) [PSYoungGen: 79750K->19587K(116736K)] 329330K->288919K(466432K), 0.0034292 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
2021-12-26T12:29:48.865-0800: 0.626: [GC (Allocation Failure) [PSYoungGen: 78254K->19476K(116736K)] 347585K->305633K(466432K), 0.0024579 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2021-12-26T12:29:48.876-0800: 0.637: [GC (Allocation Failure) [PSYoungGen: 78146K->20006K(116736K)] 364303K->322818K(466432K), 0.0027379 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
2021-12-26T12:29:48.879-0800: 0.639: [Full GC (Ergonomics) [PSYoungGen: 20006K->0K(116736K)] [ParOldGen: 302812K->260022K(349696K)] 322818K->260022K(466432K), [Metaspace: 2560K->2560K(1056768K)], 0.0277829 secs] [Times: user=0.19 sys=0.00, real=0.03 secs]
2021-12-26T12:29:48.917-0800: 0.678: [GC (Allocation Failure) [PSYoungGen: 58880K->19931K(116736K)] 318902K->279953K(466432K), 0.0018048 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2021-12-26T12:29:48.929-0800: 0.690: [GC (Allocation Failure) [PSYoungGen: 78811K->20637K(116736K)] 338833K->300307K(466432K), 0.0028534 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2021-12-26T12:29:48.942-0800: 0.703: [GC (Allocation Failure) [PSYoungGen: 79517K->20780K(116736K)] 359187K->320023K(466432K), 0.0038547 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
2021-12-26T12:29:48.955-0800: 0.715: [GC (Allocation Failure) [PSYoungGen: 79545K->18244K(116736K)] 378788K->337568K(466432K), 0.0036552 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
2021-12-26T12:29:48.959-0800: 0.719: [Full GC (Ergonomics) [PSYoungGen: 18244K->0K(116736K)] [ParOldGen: 319323K->283238K(349696K)] 337568K->283238K(466432K), [Metaspace: 2560K->2560K(1056768K)], 0.0354562 secs] [Times: user=0.21 sys=0.01, real=0.04 secs]
2021-12-26T12:29:49.006-0800: 0.766: [GC (Allocation Failure) [PSYoungGen: 58248K->21293K(116736K)] 341487K->304531K(466432K), 0.0028698 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
2021-12-26T12:29:49.019-0800: 0.779: [GC (Allocation Failure) [PSYoungGen: 79918K->22280K(116736K)] 363157K->324612K(466432K), 0.0035632 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2021-12-26T12:29:49.033-0800: 0.793: [GC (Allocation Failure) [PSYoungGen: 81160K->18825K(116736K)] 383492K->343018K(466432K), 0.0050360 secs] [Times: user=0.03 sys=0.00, real=0.00 secs]
2021-12-26T12:29:49.038-0800: 0.798: [Full GC (Ergonomics) [PSYoungGen: 18825K->0K(116736K)] [ParOldGen: 324193K->299619K(349696K)] 343018K->299619K(466432K), [Metaspace: 2560K->2560K(1056768K)], 0.0324275 secs] [Times: user=0.19 sys=0.01, real=0.04 secs]
2021-12-26T12:29:49.082-0800: 0.842: [GC (Allocation Failure) [PSYoungGen: 58839K->21598K(116736K)] 358459K->321218K(466432K), 0.0027108 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2021-12-26T12:29:49.095-0800: 0.855: [GC (Allocation Failure) [PSYoungGen: 80390K->19606K(116736K)] 380010K->339237K(466432K), 0.0037413 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
2021-12-26T12:29:49.099-0800: 0.859: [Full GC (Ergonomics) [PSYoungGen: 19606K->0K(116736K)] [ParOldGen: 319630K->308062K(349696K)] 339237K->308062K(466432K), [Metaspace: 2560K->2560K(1056768K)], 0.0329087 secs] [Times: user=0.20 sys=0.00, real=0.04 secs]
2021-12-26T12:29:49.143-0800: 0.903: [GC (Allocation Failure) [PSYoungGen: 58816K->16859K(116736K)] 366879K->324922K(466432K), 0.0027406 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2021-12-26T12:29:49.155-0800: 0.915: [GC (Allocation Failure) [PSYoungGen: 75739K->17677K(116736K)] 383802K->342453K(466432K), 0.0029850 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
2021-12-26T12:29:49.158-0800: 0.918: [Full GC (Ergonomics) [PSYoungGen: 17677K->0K(116736K)] [ParOldGen: 324776K->312157K(349696K)] 342453K->312157K(466432K), [Metaspace: 2560K->2560K(1056768K)], 0.0325338 secs] [Times: user=0.21 sys=0.00, real=0.04 secs]
2021-12-26T12:29:49.203-0800: 0.964: [GC (Allocation Failure) [PSYoungGen: 58880K->18231K(117760K)] 371037K->330389K(467456K), 0.0022387 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2021-12-26T12:29:49.215-0800: 0.976: [GC (Allocation Failure) [PSYoungGen: 78135K->37327K(116736K)] 390293K->349485K(466432K), 0.0034790 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]
2021-12-26T12:29:49.227-0800: 0.988: [GC (Allocation Failure) [PSYoungGen: 96949K->23482K(119296K)] 409107K->370976K(468992K), 0.0099279 secs] [Times: user=0.03 sys=0.02, real=0.01 secs]
2021-12-26T12:29:49.238-0800: 0.998: [Full GC (Ergonomics) [PSYoungGen: 23482K->0K(119296K)] [ParOldGen: 347493K->321756K(349696K)] 370976K->321756K(468992K), [Metaspace: 2560K->2560K(1056768K)], 0.0328866 secs] [Times: user=0.21 sys=0.00, real=0.04 secs]
2021-12-26T12:29:49.282-0800: 1.043: [Full GC (Ergonomics) [PSYoungGen: 60818K->0K(119296K)] [ParOldGen: 321756K->327802K(349696K)] 382574K->327802K(468992K), [Metaspace: 2560K->2560K(1056768K)], 0.0330792 secs] [Times: user=0.22 sys=0.01, real=0.03 secs]
2021-12-26T12:29:49.327-0800: 1.088: [Full GC (Ergonomics) [PSYoungGen: 61332K->0K(119296K)] [ParOldGen: 327802K->330183K(349696K)] 389135K->330183K(468992K), [Metaspace: 2560K->2560K(1056768K)], 0.0325230 secs] [Times: user=0.23 sys=0.00, real=0.04 secs]
2021-12-26T12:29:49.373-0800: 1.133: [Full GC (Ergonomics) [PSYoungGen: 61440K->0K(119296K)] [ParOldGen: 330183K->334786K(349696K)] 391623K->334786K(468992K), [Metaspace: 2560K->2560K(1056768K)], 0.0319694 secs] [Times: user=0.22 sys=0.00, real=0.03 secs]
2021-12-26T12:29:49.417-0800: 1.178: [Full GC (Ergonomics) [PSYoungGen: 61400K->0K(119296K)] [ParOldGen: 334786K->342656K(349696K)] 396186K->342656K(468992K), [Metaspace: 2560K->2560K(1056768K)], 0.0349932 secs] [Times: user=0.23 sys=0.00, real=0.04 secs]
Heap
PSYoungGen total 119296K, used 2594K [0x00000007b5580000, 0x00000007c0000000, 0x00000007c0000000)
eden space 61440K, 4% used [0x00000007b5580000,0x00000007b58089d8,0x00000007b9180000)
from space 57856K, 0% used [0x00000007bc780000,0x00000007bc780000,0x00000007c0000000)
to space 55296K, 0% used [0x00000007b9180000,0x00000007b9180000,0x00000007bc780000)
ParOldGen total 349696K, used 342656K [0x00000007a0000000, 0x00000007b5580000, 0x00000007b5580000)
object space 349696K, 97% used [0x00000007a0000000,0x00000007b4ea00e0,0x00000007b5580000)
Metaspace used 2567K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 276K, capacity 386K, committed 512K, reserved 1048576K
2.2.1 Minor GC日志分析
为了更好的分析老年代,我这里用第5
次GC
日志分析
2021-12-26T12:29:48.662-0800: 0.422: [GC (Allocation Failure)
[PSYoungGen: 153083K->21500K(153088K)] 302776K->213889K(502784K), 0.0130427 secs]
[Times: user=0.04 sys=0.05, real=0.01 secs]
解读如下:
-
2021-12-26T12:29:48.662-0800: 0.422
–GC
事件开始的时间。 -
GC
– 用来区分Minor GC
还是Full GC
的标志。这里是一次小型GC
(Minor GC
) 。 -
PSYoungGen
– 垃圾收集器的名称。这个名字表示的是在年轻代中使用的,表示这是并行的标记-复制(mark-copy
) ,全线暂停(STW
) 垃圾收集器。153083K->21500K(153088K)
表示GC
前的年轻代使用量、GC
后年轻代的使用量以及年轻代的总大小,简单计算GC
后的年轻代使用率21500K / 153088K = 14%
。 -
302776K->213889K(502784K)
则是GC
前后整个堆内存的使用量,以及此时可用堆的总大小,GC
后堆内存使用率为213889K / 502784K = 42.5%
。 -
[Times: user=0.04 sys=0.05, real=0.01 secs]
–GC
事件的持续时间,通过三个部分来衡量:user
表示GC
线程所消耗的总CPU
时间,sys
表示操作系统调用和系统等待事件所消耗的时间;real
则表示应用程序实际暂停的时间。因为并不是所有的操作过程都能全部并行,所以在Parallel GC
中,real
约等于user + system /GC
线程数 。
通过这部分日志可以简单算出:在GC
之前,堆内存总使用量为 302776K
,其中年轻代为 153083K
,那么可以算出刚开始老年代使用量为 302776K - 153083K = 149693K
。
在此次GC
完成后,年轻代使用量减少了 153083K - 21500K = 131583K
,总的堆内存使用量减少了302776K - 213889K = 88887K
。 那么我们可以计算出本次GC
有 131583K - 88887K = 27754K
的对象从年轻代提升到老年代。年轻代减少的内存 - 堆内存减少的内存 = 年轻代提升到老年代的内存
GC
后老年代的使用量为: 213889K - 21500K = 192389K
(GC
后堆内存使用量 - GC
后年轻代使用量)。 老年代的总大小为: 502784K - 153088K = 349696K
(堆内存总大小 - 年轻代总大小) ,使用率为: 192389K / 349696K = 55%
,已经占用一半了。
总结:关于年轻代GC
,我们关注暂停时间以及GC
后的内存使用率是否正常即可,不用特别关注GC
前的使用量,而且只要业务在运行,年轻代的对象分配就少不了,回收量也就不会少。
2.2.2 Full GC日志分析
2021-12-26T12:29:48.793-0800: 0.553: [Full GC (Ergonomics)
[PSYoungGen: 23416K->0K(116736K)]
[ParOldGen: 317113K->232542K(349696K)] 340530K->232542K(466432K),
[Metaspace: 2560K->2560K(1056768K)], 0.0268958 secs]
[Times: user=0.17 sys=0.00, real=0.02 secs]
解读如下:
-
2021-12-26T12:29:48.793-0800:
–GC
事件开始的时间。 -
Full GC
– 完全GC
的标志。Full GC
表明本次GC
清理年轻代和老年代,Ergonomics
是触发GC
的原因,表示JVM
内部环境认为此时可以进行一次垃圾收集。 -
[PSYoungGen: 23416K->0K(116736K)]
– 和上面的示例一样,清理年轻代的垃圾收集器是为 “PSYoungGen
” 的STW
收集器,采用标记-复制(mark-copy
) 算法。年轻代使用量从23416K
变为0
,一般Full GC
中年轻代的结果都是这样。 -
ParOldGen
– 用于清理老年代空间的垃圾收集器类型。在这里使用的是名为ParOldGen
的垃 圾收集器,这是一款并行STW
垃圾收集器,算法为标记-清除-整理(mark-sweep-compact
) 。317113K->232542K(349696K)
– 在GC
前后老年代内存的使用情况以及老年代空间大小。简单计 算一下,GC
之前,老年代使用率为317113K/349696K = 90%
,GC
后老年代使用率232542K / 349696K = 66%
,确实回收了不少。
那么有多少内存提升到老年代呢?其实在Full GC
里面不好算, 而在Minor GC
之中比较好算,因为Full GC
清理的是整个堆内存。
举个例子,若堆内存的减少100K
,年轻代减少180K
,可能有年轻代有80K
提升到老年代的内存,也可能是年轻代有110K
提升(70K
被清理),而老年代有30K
被清理。
若堆内存减少200K
,年轻代减少100K
,可能是年轻代提升了20K
(80K
被清理),老年代清理了120K
,也可能年轻代全部提升,老年代清理了200K
。
总之,Full GC
虽然可以计算提升数值,但是有多少内存从年轻代提升到老年代是没有具体套路,得看减出来的差值哪个大,然后具体分析,比较麻烦。但是在Minor GC
中,年轻代减少的内存 - 堆内存减少的内存 = 年轻代提升到老年代的内存,这是一定的。
-
340530K->232542K(466432K)
– 在垃圾收集之前和之后堆内存的使用情况,以及可用堆内存的总容量。简单分析可知,GC
之前堆内存使用率为:340530K/ 466432K = 73%
,GC
之后堆内存的使用率为:232542K / 466432K = 50%
。 -
[Metaspace: 2560K->2560K(1056768K)]
– 前面我们也看到了关于Metaspace
空间的类似信息。可以看出,在GC
事件中Metaspace
里面没有回收任何对象,熟悉Java
内存模型的朋友肯定知道,Metaspace
元数据区属于非堆,GC
根本不管理,当然内存没有变化。 -
0.0268958secs
–GC
事件持续的时间,以秒为单位。 -
[Times: user=0.17 sys=0.00, real=0.02 secs]
–GC
事件的持续时间,含义参见前面。
总结: Full GC
和 Minor GC
的区别是很明显的 —— Full GC
除了处理年轻代,还清理了老年代,Full GC
时我们更关注老年代的使用量有没有下降,以及下降了多少。如果Full GC
之后内存不怎么下降,使用率还很高,那就说明系统有问题了。
3. CMS GC
3.1 CMS垃圾收集器介绍
CMS GC
的官方名称为 “Mostly Concurrent Mark and Sweep Garbage Collector
”(最大并发-标记-清除-垃圾收集器)。其对年轻代采用并行STW
方式的 mark-copy
(标记-复制)算法 ,对老年代主要使用并发 mark-sweep
(标记-清除)算法 。 CMS GC
的设计目标是避免在老年代垃圾收集时出现长时间的卡顿,主要通过两种手段来达成此目标:
- 第一,不对老年代进行整理,而是使用空闲列表(
free-lists
)来管理内存空间的回收。 - 第二,在
mark-and-sweep
(标记-清除) 阶段的大部分工作和应用线程一起并发执行。
也就是说,在这些阶段并没有明显的应用线程暂停。但值得注意的是,它仍然和应用线程争抢CPU
时间。默认情况下,CMS
使用的并发线程数等于CPU
核心数的 1/4
。
通过以下选项来指定CMS
垃圾收集器:
-XX:+UseConcMarkSweepGC
如果服务器是多核CPU
,并且主要调优目标是降低GC
停顿导致的系统延迟,那么使用CMS
是个很明智的选择。通过减少每一次GC
停顿的时间,很多时候会直接改善系统的用户体验。因为多数时候都有部分CPU
资源被GC
消耗,所以在CPU
资源受限的情况下,CMS GC
会比并行GC
的吞吐量差一些(对于绝大部分系统, 这个吞吐和延迟的差别应该都不明显)。
3.2 CMS GC日志解读
java -XX:+UseConcMarkSweepGC -Xms512m -Xmx512m
-Xloggc:gc.demo.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps GCLogAnalysis
让我们看看CMS GC
的垃圾收集日志(由于写作的原因,这里是mac
系统,日志开头显示了bsd-amd64
)
Java HotSpot(TM) 64-Bit Server VM (25.202-b08) for bsd-amd64 JRE (1.8.0_202-b08), built on Dec 15 2018 20:16:16 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 8388608k(20340k free)
/proc/meminfo:
CommandLine flags: -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:MaxNewSize=178958336 -XX:MaxTenuringThreshold=6 -XX:NewSize=178958336 -XX:OldPLABSize=16 -XX:OldSize=357912576 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
2022-01-09T15:24:32.400-0800: 0.426: [GC (Allocation Failure) 2022-01-09T15:24:32.401-0800: 0.426: [ParNew: 139776K->17471K(157248K), 0.0176261 secs] 139776K->51073K(506816K), 0.0179060 secs] [Times: user=0.03 sys=0.04, real=0.01 secs]
2022-01-09T15:24:32.443-0800: 0.468: [GC (Allocation Failure) 2022-01-09T15:24:32.443-0800: 0.468: [ParNew: 157247K->17472K(157248K), 0.0582665 secs] 190849K->100725K(506816K), 0.0583472 secs] [Times: user=0.07 sys=0.05, real=0.06 secs]
2022-01-09T15:24:32.522-0800: 0.548: [GC (Allocation Failure) 2022-01-09T15:24:32.522-0800: 0.548: [ParNew: 157079K->17472K(157248K), 0.0432975 secs] 240333K->147550K(506816K), 0.0433752 secs] [Times: user=0.14 sys=0.01, real=0.04 secs]
2022-01-09T15:24:32.586-0800: 0.612: [GC (Allocation Failure) 2022-01-09T15:24:32.586-0800: 0.612: [ParNew: 157248K->17471K(157248K), 0.0376257 secs] 287326K->196615K(506816K), 0.0377060 secs] [Times: user=0.19 sys=0.02, real=0.04 secs]
2022-01-09T15:24:32.624-0800: 0.650: [GC (CMS Initial Mark) [1 CMS-initial-mark: 179144K(349568K)] 196759K(506816K), 0.0001415 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:32.624-0800: 0.650: [CMS-concurrent-mark-start]
2022-01-09T15:24:32.627-0800: 0.653: [CMS-concurrent-mark: 0.003/0.003 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:32.627-0800: 0.653: [CMS-concurrent-preclean-start]
2022-01-09T15:24:32.628-0800: 0.653: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:32.628-0800: 0.653: [CMS-concurrent-abortable-preclean-start]
2022-01-09T15:24:32.649-0800: 0.674: [GC (Allocation Failure) 2022-01-09T15:24:32.649-0800: 0.675: [ParNew: 156771K->17470K(157248K), 0.0419748 secs] 335915K->241582K(506816K), 0.0420578 secs] [Times: user=0.15 sys=0.02, real=0.05 secs]
2022-01-09T15:24:32.708-0800: 0.734: [GC (Allocation Failure) 2022-01-09T15:24:32.708-0800: 0.734: [ParNew: 157246K->17471K(157248K), 0.0410544 secs] 381358K->288636K(506816K), 0.0415380 secs] [Times: user=0.18 sys=0.02, real=0.05 secs]
2022-01-09T15:24:32.769-0800: 0.795: [GC (Allocation Failure) 2022-01-09T15:24:32.769-0800: 0.795: [ParNew: 157247K->17471K(157248K), 0.0396990 secs] 428412K->327152K(506816K), 0.0397807 secs] [Times: user=0.19 sys=0.02, real=0.04 secs]
2022-01-09T15:24:32.829-0800: 0.854: [GC (Allocation Failure) 2022-01-09T15:24:32.829-0800: 0.854: [ParNew: 157247K->157247K(157248K), 0.0000394 secs]2022-01-09T15:24:32.829-0800: 0.854: [CMS2022-01-09T15:24:32.829-0800: 0.854: [CMS-concurrent-abortable-preclean: 0.006/0.201 secs] [Times: user=0.61 sys=0.07, real=0.20 secs]
(concurrent mode failure): 309680K->257052K(349568K), 0.1563297 secs] 466928K->257052K(506816K), [Metaspace: 2560K->2560K(1056768K)], 0.1564548 secs] [Times: user=0.07 sys=0.05, real=0.16 secs]
2022-01-09T15:24:33.192-0800: 1.218: [GC (Allocation Failure) 2022-01-09T15:24:33.192-0800: 1.218: [ParNew: 139591K->17468K(157248K), 0.0072463 secs] 396643K->306430K(506816K), 0.0073297 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2022-01-09T15:24:33.200-0800: 1.225: [GC (CMS Initial Mark) [1 CMS-initial-mark: 288961K(349568K)] 306731K(506816K), 0.0001332 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.200-0800: 1.226: [CMS-concurrent-mark-start]
2022-01-09T15:24:33.201-0800: 1.227: [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.201-0800: 1.227: [CMS-concurrent-preclean-start]
2022-01-09T15:24:33.202-0800: 1.228: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.202-0800: 1.228: [CMS-concurrent-abortable-preclean-start]
2022-01-09T15:24:33.221-0800: 1.246: [GC (Allocation Failure) 2022-01-09T15:24:33.221-0800: 1.246: [ParNew: 157244K->17470K(157248K), 0.0328066 secs] 446206K->342268K(506816K), 0.0328842 secs] [Times: user=0.07 sys=0.01, real=0.03 secs]
2022-01-09T15:24:33.254-0800: 1.280: [CMS-concurrent-abortable-preclean: 0.001/0.052 secs] [Times: user=0.09 sys=0.01, real=0.05 secs]
2022-01-09T15:24:33.254-0800: 1.280: [GC (CMS Final Remark) [YG occupancy: 25303 K (157248 K)]2022-01-09T15:24:33.255-0800: 1.280: [Rescan (parallel) , 0.0003075 secs]2022-01-09T15:24:33.255-0800: 1.280: [weak refs processing, 0.0000297 secs]2022-01-09T15:24:33.255-0800: 1.281: [class unloading, 0.0002089 secs]2022-01-09T15:24:33.255-0800: 1.281: [scrub symbol table, 0.0002644 secs]2022-01-09T15:24:33.255-0800: 1.281: [scrub string table, 0.0001460 secs][1 CMS-remark: 324797K(349568K)] 350101K(506816K), 0.0010829 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.256-0800: 1.281: [CMS-concurrent-sweep-start]
2022-01-09T15:24:33.256-0800: 1.282: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.256-0800: 1.282: [CMS-concurrent-reset-start]
2022-01-09T15:24:33.256-0800: 1.282: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 157248K, used 156429K [0x00000007a0000000, 0x00000007aaaa0000, 0x00000007aaaa0000)
eden space 139776K, 99% used [0x00000007a0000000, 0x00000007a87b3c58, 0x00000007a8880000)
from space 17472K, 99% used [0x00000007a9990000, 0x00000007aaa9fa68, 0x00000007aaaa0000)
to space 17472K, 0% used [0x00000007a8880000, 0x00000007a8880000, 0x00000007a9990000)
concurrent mark-sweep generation total 349568K, used 285724K [0x00000007aaaa0000, 0x00000007c0000000, 0x00000007c0000000)
Metaspace used 2567K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 276K, capacity 386K, committed 512K, reserved 1048576K
可以看到,比起串行GC
/并行GC
来说,CMS
的日志信息复杂了很多,这一方面是因为 CMS
拥有更加精细的GC
步骤,另一方面GC
日志很详细就意味着暴露出来的信息也就更全面细致。
3.2.1 Minor GC日志分析
2022-01-09T15:24:32.443-0800: 0.468: [GC (Allocation Failure)
2022-01-09T15:24:32.443-0800: 0.468: [ParNew: 157247K->17472K(157248K), 0.0582665 secs]
190849K->100725K(506816K), 0.0583472 secs]
[Times: user=0.07 sys=0.05, real=0.06 secs]
解读如下:
-
2022-01-09T15:24:32.443-0800: 0.468:
–GC
事件开始的时间。 -
GC (Allocation Failure)
– 用来区分Minor GC
还是Full GC
的标志。GC
表明这是一次小型GC
,Allocation Failure
表示触发GC
的原因。本次GC
事件,是由于年轻代可用空间不足,新对象的内存分配失败引起的。 -
[ParNew: 157247K->17472K(157248K), 0.0582665 secs]
- 其中ParNew
是垃圾收集器的名称,对应的就是前面日志中打印的-XX:+UseParNewGC
这个命令行标志。表示在年轻代中使用的并行的标记-复制(mark-copy
) 垃圾收集器,这是专门设计了用来配合CMS
垃圾收集器,因为CMS
只负责回收老年代。后面的数字表示GC
前后的年轻代使用量,以及年轻代的总大小。0.0582665 secs
是消耗的时间。 -
190849K->100725K(506816K), 0.0583472 secs
– 表示GC
前后堆内存的使用量,以及堆内存空间的大小。消耗的时间是0.0583472 secs
,和前面的ParNew
部分的时间基本上一样。 -
[Times: user=0.07 sys=0.05, real=0.06 secs]
–GC
事件的持续时间。user
是GC
线程所消耗的总CPU
时间;sys
是操作系统调用和系统等待事件消耗的时间; 应用程序实际暂停的时间real ≈ (user + sys) / GC
线程数 。因为并不是所有线程都用来GC
,总有一定比例的处理过程是不能并行执行的。
进一步计算和分析可以得知,在GC
之前,年轻代使用量为 157247K / 157248K = 99.9%
。堆内存的使用率为 190849K / 506816K = 37.7%
。稍微估算一下,GC
前老年代的使用率为: (190849K-157247K) / (506816K-157248K) = (33602K /349568K) = 9.6%
。GC
后年轻代使用量为 17472K / 157248K = 11%
,下降了 139775K
。堆内存使用量为 100725K / 506816K = 19.8%
,只下降了 90124K
。年轻代减少的内存 - 堆内存减少的内存 = 年轻代提升到老年代的内存,则139775K - 90124K = 49651K
。
3.2.2 Full GC日志分析
CMS
的日志是一种完全不同的格式,并且很长,因为CMS
对老年代进行垃圾收集时每个阶段都会有自己的日志。为了简洁,我们将对这部分日志按照阶段依次介绍
2022-01-09T15:24:33.200-0800: 1.225: [GC (CMS Initial Mark)
[1 CMS-initial-mark: 288961K(349568K)] 306731K(506816K), 0.0001332 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.200-0800: 1.226: [CMS-concurrent-mark-start]
2022-01-09T15:24:33.201-0800: 1.227: [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.201-0800: 1.227: [CMS-concurrent-preclean-start]
2022-01-09T15:24:33.202-0800: 1.228: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.202-0800: 1.228: [CMS-concurrent-abortable-preclean-start]
2022-01-09T15:24:33.221-0800: 1.246: [GC (Allocation Failure) 2022-01-09T15:24:33.221-0800: 1.246: [ParNew: 157244K->17470K(157248K), 0.0328066 secs] 446206K->342268K(506816K), 0.0328842 secs] [Times: user=0.07 sys=0.01, real=0.03 secs]
2022-01-09T15:24:33.254-0800: 1.280: [CMS-concurrent-abortable-preclean: 0.001/0.052 secs] [Times: user=0.09 sys=0.01, real=0.05 secs]
2022-01-09T15:24:33.254-0800: 1.280: [GC (CMS Final Remark)
[YG occupancy: 25303 K (157248 K)]
2022-01-09T15:24:33.255-0800: 1.280:
[Rescan (parallel) , 0.0003075 secs]
2022-01-09T15:24:33.255-0800: 1.280:
[weak refs processing, 0.0000297 secs]
2022-01-09T15:24:33.255-0800: 1.281:
[class unloading, 0.0002089 secs]
2022-01-09T15:24:33.255-0800: 1.281:
[scrub symbol table, 0.0002644 secs]
2022-01-09T15:24:33.255-0800: 1.281:
[scrub string table, 0.0001460 secs]
[1 CMS-remark: 324797K(349568K)] 350101K(506816K), 0.0010829 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.256-0800: 1.281: [CMS-concurrent-sweep-start]
2022-01-09T15:24:33.256-0800: 1.282: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-09T15:24:33.256-0800: 1.282: [CMS-concurrent-reset-start]
2022-01-09T15:24:33.256-0800: 1.282: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
在实际情况下,进行老年代的并发回收时,可能会伴随着多次年轻代的minor GC
。在这种情况下,full GC
的日志中就会掺杂着多次minor GC
事件。
3.2.2.1 阶段 1: Initial Mark(初始标记) (有STW)
这个阶段伴随着STW
暂停。初始标记的目标是标记所有的根对象,包括根对象直接引用的对象,以及被年轻代中所有存活对象所引用的对象(老年代单独回收),见下图。
【为什么CMS
不管年轻代了呢? 前面不是刚刚完成minor GC
嘛,再去收集年轻代估计也没什么效果】。
先看这个阶段的日志:
2022-01-09T15:24:33.200-0800: 1.225:
[GC (CMS Initial Mark)
[1 CMS-initial-mark: 288961K(349568K)] 306731K(506816K), 0.0001332 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
本阶段日志解读如下:
-
2022-01-09T15:24:33.200-0800: 1.225:
– 时间部分就不讲了,参考前面的解读。后面的其他阶段也一样,不再进行重复介绍。 -
CMS Initial Mark
– 这个阶段的名称为 “Initial Mark
”,会标记所有的GC Root
。 -
[1 CMS-initial-mark: 288961K(349568K)]
– 这部分数字表示老年代的使用量,以及老年代的空间大小。 -
306731K(506816K),0.0001332 secs
– 当前堆内存的使用量,以及可用堆的大小、消耗的时间。可以看出这个时间非常短,只有0.1
毫秒左右,因为要标记的这些Root
数量很少。 -
[Times: user=0.00 sys=0.00, real=0.00 secs]
– 初始标记事件暂停的时间,都是0
可以忽略不计。
3.2.2.2 阶段 2: Concurrent Mark(并发标记)
在此阶段,CMS GC
遍历老年代,标记所有的存活对象,从前一阶段 “Initial Mark
” 找到的根对象开始算起。
所谓“并发标记”阶段,就是与应用程序同时运行,不用暂停的阶段。 请注意,并非所有老年代中存活的对象都在此阶段被标记,因为在标记过程中对象的引用关系还在发生变化。
在上面的示意图中, “ 当前处理的对象 ” 的一个引用就被应用线程给断开了,即这个部分的对象关系发生了变化(后面会讲如何处理)。
先看这个阶段的日志:
2022-01-09T15:24:33.200-0800: 1.226: [CMS-concurrent-mark-start]
2022-01-09T15:24:33.201-0800: 1.227:
[CMS-concurrent-mark: 0.001/0.001 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
本阶段日志解读如下:
-
CMS-concurrent-mark
– 指明了是CMS
垃圾收集器所处的阶段为并发标记(“Concurrent Mark
”) 。 -
0.001/0.001 secs
– 此阶段的持续时间,分别是GC
线程消耗的时间和实际消耗的时间。 -
[Times: user=0.00 sys=0.00, real=0.00 secs]
–Times
对并发阶段来说这些时间并没多少意义,因为是从并发标记开始时刻计算的,而这段时间应用线程也在执行,所以这个时间只是一个大概的值。
3.2.2.3 阶段 3: Concurrent Preclean(并发预清理)
此阶段同样是与应用线程并发执行的,不需要停止应用线程。 因为前一阶段【并发标记】与程序并发运行,可能有一些引用关系已经发生了改变。如果在并发标记过程中引用关系发生了变化,JVM
会通过“ Card
(卡片)”的方式将引用关系发生了改变的区域标记为“脏”区,这就是所谓的卡片标记(Card Marking
)
在预清理阶段,这些脏对象会被统计出来,他们所引用的对象也会被标记。此阶段完成后,用以标记的 card
也就会被清空。
此外,本阶段也会进行一些必要的细节处理, 还会为 Final Remark
阶段做一些准备工作。
看看本阶段的日志:
2022-01-09T15:24:33.201-0800: 1.227: [CMS-concurrent-preclean-start]
2022-01-09T15:24:33.202-0800: 1.228:
[CMS-concurrent-preclean: 0.001/0.001 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
简单解读本阶段日志:
-
CMS-concurrent-preclean
– 表明这是并发预清理阶段的日志,这个阶段会统计前面的并发标 记阶段执行过程中引用关系发生了改变的对象。 -
0.001/0.001 secs
– 此阶段的持续时间,分别是GC
线程运行时间和实际占用的时间。 -
[Times: user=0.00 sys=0.00, real=0.00 secs]
–Times
这部分对并发阶段来说没多少意义,因为是从开始时间计算的,而这段时间内不仅GC
线程在执行并发预清理,应用线程也在运行。
3.2.2.4 阶段 4: Concurrent Abortable Preclean(可取消的并发预清理)
此阶段也不停止应用线程。本阶段尝试在 STW
的 Final Remark
阶段之前尽可能地多做一些工作。本阶段的具体时间取决于多种因素,因为它循环做同样的事情,直到满足某个退出条件( 如迭代次数,有用工作量, 消耗的系统时间等等)。
看看本阶段的日志:
2022-01-09T15:24:33.202-0800: 1.228: [CMS-concurrent-abortable-preclean-start]
2022-01-09T15:24:33.221-0800: 1.246: [GC (Allocation Failure)
2022-01-09T15:24:33.221-0800: 1.246: [ParNew: 157244K->17470K(157248K), 0.0328066 secs]
446206K->342268K(506816K), 0.0328842 secs]
[Times: user=0.07 sys=0.01, real=0.03 secs]
2022-01-09T15:24:33.254-0800: 1.280:
[CMS-concurrent-abortable-preclean: 0.001/0.052 secs]
[Times: user=0.09 sys=0.01, real=0.05 secs]
开头就说了,在CMS
垃圾回收器中,Full GC
的日志中就会掺杂着多次Minor GC
,这里有一个Minor GC
,我们只分析Full GC
就好了。
简单解读:
-
CMS-concurrent-abortable-preclean
– 指示此阶段的名称:“Concurrent Abortable Preclean
”。 -
0.001/0.052 secs
– 此阶段GC
线程的运行时间和实际占用的时间。从本质上讲,GC
线程试图 在执行STW
暂停之前等待尽可能长的时间。默认条件下,此阶段可以持续最长5
秒钟的时间。 -
[Times: user=0.09 sys=0.01, real=0.05 secs]
– “Times
” 这部分对并发阶段来说没多少意义,因为程序在并发阶段中持续运行。
此阶段可能显著影响STW
停顿的持续时间,并且有许多重要的配置选项和失败模式。
3.2.2.5 阶段 5: Final Remark(最终标记) (有STW)
最终标记阶段是此次GC
事件中的第二次(也是最后一次)STW
停顿。 本阶段的目标是完成老年代中所有存活对象的标记。因为之前的预清理阶段是并发执行的,有可能GC
线程跟不上应用程序的修改速度。所以需要一次 STW
暂停来处理各种复杂的情况。
通常CMS
会尝试在年轻代尽可能空的情况下执行 Final Remark
阶段,以免连续触发多次 STW
事件。
来看看本阶段日志,相对复杂一点
2022-01-09T15:24:33.254-0800: 1.280: [GC (CMS Final Remark)
[YG occupancy: 25303 K (157248 K)]
2022-01-09T15:24:33.255-0800: 1.280: [Rescan (parallel) , 0.0003075 secs]
2022-01-09T15:24:33.255-0800: 1.280: [weak refs processing, 0.0000297 secs]
2022-01-09T15:24:33.255-0800: 1.281: [class unloading, 0.0002089 secs]
2022-01-09T15:24:33.255-0800: 1.281: [scrub symbol table, 0.0002644 secs]
2022-01-09T15:24:33.255-0800: 1.281: [scrub string table, 0.0001460 secs]
[1 CMS-remark: 324797K(349568K)] 350101K(506816K), 0.0010829 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
日志解读如下:
-
CMS Final Remark
– 这是此阶段的名称,最终标记阶段,会标记老年代中所有的存活对象,包 括此前的并发标记过程中创建/修改的引用。 -
YG occupancy: 25303 K (157248 K)
– 当前年轻代的使用量和总容量。 -
[Rescan (parallel) , 0.0003075 secs]
– 在程序暂停后进行重新扫描(Rescan)
,以完成存活对象的标记。这部分是并行执行的,消耗的时间为0.0003075
秒 。 -
weak refs processing,0.0000297 secs
– 第一个子阶段: 处理弱引用的持续时间。 -
class unloading,0.0002089 secs
– 第二个子阶段: 卸载不使用的类,以及持续时间。 -
scrub symbol table, 0.0002644 secs
– 第三个子阶段: 清理符号表,即持有class
级别metadata
的符号表(symbol tables
)。 -
scrub string table, 0.0001460 secs
– 第四个子阶段: 清理内联字符串对应的string tables
。 -
[1 CMS-remark: 324797K(349568K)]
– 此阶段完成后老年代的使用量和总容量。 -
350101K(506816K),0.0010829 secs
– 此阶段完成后,整个堆内存的使用量和总容量。 -
[Times: user=0.00 sys=0.00, real=0.00 secs]
–GC
事件的持续时间。
在这5
个标记阶段完成后,老年代中的所有存活对象都被标记上了,接下来JVM
会将所有不使用的对象清除,以回收老年代空间。
3.2.2.6 阶段 6: Concurrent Sweep(并发清除)
此阶段与应用程序并发执行,不需要STW
停顿。JVM
在此阶段删除不再使用的对象,并回收他们占用的内存空间。
看看本阶段日志
2022-01-09T15:24:33.256-0800: 1.281: [CMS-concurrent-sweep-start]
2022-01-09T15:24:33.256-0800: 1.282:
[CMS-concurrent-sweep: 0.001/0.001 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
简单解读下日志:
-
CMS-concurrent-sweep
– 此阶段的名称,“Concurrent Sweep
”,并发清除老年代中所有未被 标记的对象、也就是不再使用的对象,以释放内存空间。 -
0.001/0.001 secs
– 此阶段的持续时间和实际占用的时间,这是一个四舍五入值,只精确到小数点后3
位。 -
[Times: user=0.00 sys=0.00, real=0.00 secs]
– “Times
”部分对并发阶段来说没有多少意义,因为是从并发标记开始时计算的,而这段时间内不仅是并发标记线程在执行,程序线程也在运行。
3.2.2.7 阶段 7: Concurrent Reset(并发重置)
此阶段与应用程序并发执行,重置CMS
算法相关的内部数据,为下一次GC
循环做准备。
看看本阶段日志:
2022-01-09T15:24:33.256-0800: 1.282: [CMS-concurrent-reset-start]
2022-01-09T15:24:33.256-0800: 1.282:
[CMS-concurrent-reset: 0.000/0.000 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
简单解读下日志:
-
CMS-concurrent-reset
– 此阶段的名称,“Concurrent Reset
”,重置CMS
算法的内部数据结构,为下一次GC
循环做准备。 -
0.000/0.000 secs
– 此阶段的持续时间和实际占用的时间 -
[Times: user=0.00 sys=0.00,real=0.00 secs]
– “Times
”部分对并发阶段来说没多少意义,因为是从并发标记开始时计算的,而这段时间内不仅GC
线程在运行,程序也在运行。
那么问题来了,CMS
之后老年代内存使用量是多少呢? 很抱歉这里分析不了,只能通过后面的Minor GC
日志来分析了,需要看下次的Minor GC
的日志
总之,CMS
垃圾收集器在减少停顿时间上做了很多复杂而有用的工作,用于垃圾回收的并发线程执行的同时,并不需要暂停应用线程。 当然,CMS
也有一些缺点,其中最大的问题就是老年代内存碎片问题(因为不压缩),在某些情况下GC
会造成不可预测的暂停时间,特别是堆内存较大的情况下。
4. G1垃圾收集器
4.1 G1垃圾收集器介绍
G1
的全称是 Garbage-First
,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 GC
最主要的设计目标是:将STW
停顿的时间和分布,变成可预期且可配置的。
事实上,G1 GC
是一款软实时垃圾收集器,可以为其设置某项特定的性能指标。例如可以指定: 在任意 xx
毫秒时间范围内,STW
停顿不得超过 yy
毫秒。 举例说明: 任意 1
秒 内暂停时间不超过 5
毫秒 。G1 GC
会尽力达成这个目标(有很大概率会满足,但并不完全确定)。
为了达成可预期停顿时间的指标,G1 GC
有一些独特的实现。 首先,堆不再分成年轻代和老年代,而是划分为多个(通常是2048
个)可以存放对象的小块堆区域(smaller heap regions
) 。每个小块,可能一会被定义成 Eden
区,一会被指定为 Survivor
区或者Old
区。在逻辑上,所有的Eden
区和Survivor
区合起来就是年轻代,所有的Old
区拼在一起那就是老年代,如下图所示:
这样划分之后,使得 G1
不必每次都去收集整个堆空间,而是以增量的方式来进行处理: 每次只处理一部分内存块,称为此次GC
的回收集(collection set
)。每次GC
暂停都会收集所有年轻代的内存块,但一般只包含部分老年代的内存块,见下图带对号的部分:
G1
的另一项创新是,在并发阶段估算每个小堆块存活对象的总数。构建回收集的原则是: 垃圾最多的小块 会被优先收集。这也是G1
名称的由来。
通过以下选项来指定G1
垃圾收集器:
-XX:+UseG1GC
4.2 G1 GC常用参数设置
-
-XX:+UseG1GC
:启用G1 GC
,JDK7
和JDK8
要求必须显示申请启动G1 GC
; -
-XX:G1NewSizePercent
:初始年轻代占整个Java Heap
的大小,默认值为5%
; -
-XX:G1MaxNewSizePercent
:最大年轻代占整个Java Heap
的大小,默认值为60%
; -
-XX:G1HeapRegionSize
:设置每个Region
的大小,单位MB
,需要为1,2,4,8,16,32
中的某 个值,默认是堆内存的1/2000
。如果这个值设置比较大,那么大对象就可以进入Region
了。 -
-XX:ConcGCThreads
:与Java
应用一起执行的GC
线程数量,默认是Java
线程的1/4
,减少这个参 数的数值可能会提升并行回收的效率,提高系统内部吞吐量。如果这个数值过低,参与回收垃圾的线程不足,也会导致并行回收机制耗时加长。 -
-XX:+InitiatingHeapOccupancyPercent
(简称IHOP
):G1
内部并行回收循环启动的阈值, 默认为Java Heap
的45%
。这个可以理解为老年代使用大于等于45%
的时候,JVM
会启动垃圾回收。这个值非常重要,它决定了在什么时间启动老年代的并行回收。 -
-XX:G1HeapWastePercent
:G1
停止回收的最小内存大小,默认是堆大小的5%
。GC
会收集所有的Region
中的对象,但是如果下降到了5%
,就会停下来不再收集了。就是说,不必每次回收就把所有的垃圾都处理完,可以遗留少量的下次处理,这样也降低了单次消耗的时间。 -
-XX:G1MixedGCCountTarget
:设置并行循环之后需要有多少个混合GC
启动,默认值是8
个。老 年代Regions
的回收时间通常比年轻代的收集时间要长一些。所以如果混合收集器比较多,可以允许G1
延长老年代的收集时间。 -
-XX:+G1PrintRegionLivenessInfo
:这个参数需要和-XX:+UnlockDiagnosticVMOptions
配合启动,打印JVM
的调试信息,每个Region
里的对象存活信息。 -
-XX:G1ReservePercent
:G1
为了保留一些空间用于年代之间的提升,默认值是堆空间的10%
。 因为大量执行回收的地方在年轻代(存活时间较短),所以如果你的应用里面有比较大的堆内存空间、 比较多的大对象存活,这里需要保留一些内存。 -
-XX:+G1SummarizeRSetStats
:这也是一个VM
的调试信息。如果启用,会在VM
退出的时候打印出RSets
的详细总结信息。如果启用-XX:G1SummaryRSetStatsPeriod
参数,就会阶段性地打印RSets
信息。 -
-XX:+G1TraceConcRefinement
:这个也是一个VM
的调试信息,如果启用,并行回收阶段的日志就会被详细打印出来。 -
-XX:+GCTimeRatio
:大家知道,GC
的有些阶段是需要Stop-the-World
,即停止应用线程的。这个参数就是计算花在Java
应用线程上和花在GC
线程上的时间比率,默认是9
,跟新生代内存的分配比例一致。这个参数主要的目的是让用户可以控制花在应用上的时间,G1
的计算公式是100/ (1+GCTimeRatio)
。这样如果参数设置为9
,则最多10%
的时间会花在GC
工作上面。Parallel GC
的 默认值是99
,表示1%
的时间被用在GC
上面,这是因为Parallel GC
贯穿整个GC
,而G1
则根据Region
来进行划分,不需要全局性扫描整个内存堆。 -
-XX:+UseStringDeduplication
:手动开启Java String
对象的去重工作,这个是JDK8u20
版本之后新增的参数,主要用于相同String
避免重复申请内存,节约Region
的使用。 -
-XX:MaxGCPauseMills
:预期G1
每次执行GC
操作的暂停时间,单位是毫秒,默认值是200
毫秒,G1
会尽量保证控制在这个范围内。
这里面最重要的参数,就是:
-
-XX:+UseG1GC
:启用G1 GC
; -
-XX:+InitiatingHeapOccupancyPercent
:决定什么情况下发生G1 GC
; -
-XX:MaxGCPauseMills
:期望每次GC
暂定的时间,比如我们设置为50
,则G1 GC
会通过调节每次GC
的操作时间,尽量让每次系统的GC
停顿都在50
上下浮动。如果某次GC
时间超过50ms
, 比如说100ms
,那么系统会自动在后面动态调整GC
行为,围绕50
毫秒浮动。
4.3 G1日志解读和分析
java -XX:+UseG1GC -Xms512m -Xmx512m
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.demo.log GCLogAnalysis
让我们看看G1 GC
的垃圾收集日志(由于写作的原因,这里是windows
系统,日志开头显示了windows-amd64
)
Java HotSpot(TM) 64-Bit Server VM (25.212-b10) for windows-amd64 JRE (1.8.0_212-b10), built on Apr 1 2019 22:50:23 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16633820k(5481624k free), swap 28079616k(4538104k free)
CommandLine flags: -XX:InitialHeapSize=536870912 -XX:MaxHeapSize=536870912 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC -XX:-UseLargePagesIndividualAllocation
2022-01-10T10:42:58.694+0800: 0.203: [GC pause (G1 Evacuation Pause) (young), 0.0023921 secs]
[Parallel Time: 2.0 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 202.8, Avg: 202.8, Max: 202.9, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.4, Max: 0.9, Diff: 0.8, Sum: 1.5]
[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: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.8, Avg: 1.3, Max: 1.5, Diff: 0.7, Sum: 5.1]
[Termination (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 0.7]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 1.8, Avg: 1.9, Max: 1.9, Diff: 0.2, Sum: 7.5]
[GC Worker End (ms): Min: 204.6, Avg: 204.7, Max: 204.8, Diff: 0.2]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 25.0M(25.0M)->0.0B(21.0M) Survivors: 0.0B->4096.0K Heap: 29.2M(512.0M)->10.4M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-10T10:42:58.706+0800: 0.215: [GC pause (G1 Evacuation Pause) (young), 0.0025225 secs]
[Parallel Time: 2.3 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 215.1, Avg: 215.1, Max: 215.1, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.2, Diff: 0.0, Sum: 0.5]
[Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Processed Buffers: Min: 0, Avg: 0.3, Max: 1, Diff: 1, Sum: 1]
[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: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1.4, Avg: 1.5, Max: 1.5, Diff: 0.1, Sum: 5.8]
[Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 1.7, Avg: 1.7, Max: 1.7, Diff: 0.0, Sum: 6.7]
[GC Worker End (ms): Min: 216.8, Avg: 216.8, Max: 216.8, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 21.0M(21.0M)->0.0B(24.0M) Survivors: 4096.0K->4096.0K Heap: 36.5M(512.0M)->21.0M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
// 这里省略很多重复类似的日志...................
2022-01-10T10:42:59.662+0800: 1.171: [GC concurrent-root-region-scan-start]
2022-01-10T10:42:59.662+0800: 1.171: [GC concurrent-root-region-scan-end, 0.0000776 secs]
2022-01-10T10:42:59.662+0800: 1.171: [GC concurrent-mark-start]
2022-01-10T10:42:59.664+0800: 1.173: [GC concurrent-mark-end, 0.0019498 secs]
2022-01-10T10:42:59.664+0800: 1.173: [GC remark 2022-01-10T10:42:59.664+0800: 1.173: [Finalize Marking, 0.0000447 secs] 2022-01-10T10:42:59.664+0800: 1.174: [GC ref-proc, 0.0000356 secs] 2022-01-10T10:42:59.664+0800: 1.174: [Unloading, 0.0004159 secs], 0.0018676 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-10T10:42:59.666+0800: 1.175: [GC cleanup 379M->379M(512M), 0.0002800 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
garbage-first heap total 524288K, used 387501K [0x00000000e0000000, 0x00000000e0101000, 0x0000000100000000)
region size 1024K, 16 young (16384K), 2 survivors (2048K)
Metaspace used 2615K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
为了节省空间,这里只截取部分日志,完整日志有2900
多行
4.3.1 年轻代模式转移暂停(Evacuation Pause)
当年轻代空间用满后,应用线程会被暂停,年轻代内存块中的存活对象被拷贝到存活区。 如果还没有存活区,则任意选择一部分空闲的内存块作为存活区。 拷贝的过程称为转移(Evacuation
),这和前面介绍的其他年轻代收集器是一样的工作原理。 转移暂停的日志信息很长,为简单起见,我们去除了一些不重要的信息.。在并发阶段之后我们会进行详细的讲解。 此外,由于日志记录很多,所以并行阶段和“其他”阶段的日志将拆分为多个部分来进行讲解。
我们从GC
日志中抽取部分关键信息
2022-01-10T10:42:58.694+0800: 0.203:
[GC pause (G1 Evacuation Pause) (young), 0.0023921 secs]
[Parallel Time: 2.0 ms, GC Workers: 4]
...... worker线程的详情,下面单独讲解
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.4 ms]
...... 其他琐碎任务,下面单独讲解
[Eden: 25.0M(25.0M)->0.0B(21.0M) Survivors: 0.0B->4096.0K Heap: 29.2M(512.0M)->10.4M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
分析一下日志:
-
[GC pause (G1 Evacuation Pause) (young), 0.0023921 secs]
–G1
转移暂停,纯年轻代模式,只清理年轻代空间。这次暂停在JVM
启动之后203 ms
开始,持续的系统时间为0.0023921
秒 ,也就是2.4ms
。 -
[Parallel Time: 2.0 ms, GC Workers: 4]
– 表明后面的活动由4
个Worker
线程并行执行,消耗时间为2.0
毫秒(real time
),worker
是一种模式,类似于一个老板指挥多个工人干活。 -
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
– 释放用于管理并行活动的内部数据,一般都接近于零。这个过程是串行执行的。 -
[Clear CT: 0.0 ms]
– 清理其他部分数据,也是非常快的,如非必要基本上等于零。也是串行执行的过程。 -
[Other: 0.4 ms]
– 其他活动消耗的时间,其中大部分是并行执行的。 -
[Eden: 25.0M(25.0M)->0.0B(21.0M)
– 暂停之前Eden
区的使用量/总容量和暂停之后Eden
区的使用量/总容量 -
Survivors: 0.0B -> 4096.0K
–GC
暂停前后,存活区的使用量。 -
Heap: 29.2M(512.0M)->10.4M(512.0M)]
– 暂停前整个堆内存的使用量与总容量,暂停后整个堆内存的使用量与总容量 -
[Times: user=0.00 sys=0.00, real=0.00 secs]
–GC
事件的持续时间。
再来看看上面省略的worker
线程的日志,最繁重的GC
任务由多个专用的 worker
线程来执行。
[Parallel Time: 2.0 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 202.8, Avg: 202.8, Max: 202.9, Diff: 0.1]
[Ext Root Scanning (ms): Min: 0.2, Avg: 0.4, Max: 0.9, Diff: 0.8, Sum: 1.5]
[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: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 0.8, Avg: 1.3, Max: 1.5, Diff: 0.7, Sum: 5.1]
[Termination (ms): Min: 0.0, Avg: 0.2, Max: 0.3, Diff: 0.3, Sum: 0.7]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
[GC Worker Total (ms): Min: 1.8, Avg: 1.9, Max: 1.9, Diff: 0.2, Sum: 7.5]
[GC Worker End (ms): Min: 204.6, Avg: 204.7, Max: 204.8, Diff: 0.2]
Worker线程的日志信息解读:
-
[Parallel Time: 2.0 ms, GC Workers: 4]
– 前面介绍过,这表明下列活动由4
个线程并行执行,消耗的时间为2.0
毫秒(real time
)。 -
GC Worker Start (ms)
–GC
的worker
线程开始启动时,相对于pause
开始时间的毫秒间隔。如果Min
和Max
差别很大,则表明本机其他进程所使用的线程数量过多,挤占了GC
的可用CPU
时间。 -
Ext Root Scanning (ms)
– 用了多长时间来扫描堆外内存(non-heap
)的GC ROOT
,如classloaders
,JNI
引用,JVM
系统ROOT
等。后面显示了运行时间,“Sum
” 指的是CPU
时间。 -
Update RS
,Processed Buffers
,Scan RS
这三部分也是类似的,RS
是Remembered Set
的缩写,可以参考前面。 -
Code Root Scanning (ms)
– 扫描实际代码中的root
用了多长时间:例如线程栈中的局部变量。 -
Object Copy (ms)
– 用了多长时间来拷贝回收集中的存活对象。 -
Termination (ms)
–GC
的worker
线程用了多长时间来确保自身可以安全地停止,在这段时间内什么也不做,完成后GC
线程就终止运行了,所以叫终止等待时间。 -
Termination Attempts
–GC
的worker
线程尝试多少次try
和teminate
。如果worker
发现还有一些任务没处理完,则这一次尝试就是失败的,暂时还不能终止。 -
GC Worker Other (ms)
– 其他的小任务, 因为时间很短,在GC
日志将他们归结在一起。 -
GC Worker Total (ms)
–GC
的worker
线程工作时间总计。 -
[GC Worker End (ms)
–GC
的worker
线程完成作业时刻,相对于此次GC
暂停开始时间的毫秒数。通常来说这部分数字应该大致相等,否则就说明有太多的线程被挂起,很可能是因为“坏邻居效应(noisy neighbor)" 所导致的。
此外,在转移暂停期间,还有一些琐碎的小任务。
[Other: 0.4 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
其他琐碎任务,这里只介绍其中的一部分:
-
[Other: 0.4 ms]
– 其他活动消耗的时间,其中很多是并行执行的。 -
Choose CSet
- 选择CSet
消耗的时间;CSet
是Collection Set
的缩写。 -
[Ref Proc: 0.2 ms]
– 处理非强引用(non-strong
)的时间:进行清理或者决定是否需要清理。 -
[Ref Enq: 0.0 ms]
– 用来将剩下的non-strong
引用排列到合适的ReferenceQueue
中。 -
Humongous Register
,Humongous Reclaim
大对象相关的部分。后面进行介绍。 -
[Free CSet: 0.0 ms]
– 将回收集中被释放的小堆归还所消耗的时间,以便他们能用来分配新的对象。
此次 young GC
对应的示意图如下所示:
4.3.2 Concurrent Marking(并发标记)
G1 GC
的很多概念建立在CMS
的基础上,所以下面的内容需要对CMS
有一定的理解。G1
并发标记的过程与CMS
基本上是一样的。G1
的并发标记通过 Snapshot-At-The-Beginning
(起始快照) 的方式,在标记阶段开始时记下所有的存活对象。即使在标记的同时又有一些变成了垃圾。通过对象的存活信息,可以构建出每个小堆块的存活状态,以便回收集能高效地进行选择。 这些信息在接下来的阶段会用来执行老年代区域的垃圾收集。
有两种情况是可以完全并发执行的
- 一、如果在标记阶段确定某个小堆块中没有存活对象,只包含垃圾;
- 二、在
STW
转移暂停期间,同时包含垃圾和存活对象的老年代小堆块。
当堆内存的总体使用比例达到一定数值,就会触发并发标记。这个默认比例是 45%
,但也可以通过JVM
参数 InitiatingHeapOccupancyPercent
来设置。和CMS
一样,G1
的并发标记也是由多个阶段组成, 其中一些阶段是完全并发的,还有一些阶段则会暂停应用线程。
4.3.2.1 阶段 1: Initial Mark(初始标记)
此阶段标记所有从GC
根对象直接可达的对象。在CMS
中需要一次STW
暂停,但G1
里面通常是在转移暂停的同时处理这些事情,所以它的开销是很小的。
可以在 Evacuation Pause 日志中的第一行看到(initial-mark)暂停,类似这样:
2022-01-10T10:42:58.694+0800: 0.203: [GC pause (G1 Evacuation Pause) (young), 0.0023921 secs]
这里引发GC
的原因是Evacuation Pause
,表示标记可达对象产生的暂停,也可能是其他原因,例如: to-space exhausted
,或者默认GC
原因等等。
4.3.2.2 阶段 2: Root Region Scan(Root区扫描)
此阶段标记所有从 “根区域” 可达的存活对象。根区域包括:非空的区域,以及在标记过程中不得不收集的区域。
因为在并发标记的过程中迁移对象会造成很多麻烦,所以此阶段必须在下一次转移暂停之前完成。如果必须启动转移暂停,则会先要求根区域扫描中止,等它完成才能继续扫描。在当前版本的实现中,根区域是存活的小堆块:包括下一次转移暂停中肯定会被清理的那部分年轻代小堆块。
对应的日志:
2022-01-10T10:42:59.662+0800: 1.171: [GC concurrent-root-region-scan-start]
2022-01-10T10:42:59.662+0800: 1.171: [GC concurrent-root-region-scan-end, 0.0000776 secs]
4.3.2.3 阶段 3: Concurrent Mark(并发标记)
此阶段和CMS
的并发标记阶段非常类似:只遍历对象图,并在一个特殊的位图中标记能访问到的对象。
为了确保标记开始时的快照准确性,所有应用线程并发对对象图执行引用更新,G1
要求放弃前面阶段为了标记目的而引用的过时引用。
对应的日志:
2022-01-10T10:42:59.662+0800: 1.171: [GC concurrent-mark-start]
2022-01-10T10:42:59.664+0800: 1.173: [GC concurrent-mark-end, 0.0019498 secs]
4.3.2.4 阶段 4: Remark(再次标记)
和CMS
类似,这是一次STW
停顿(因为不是并发的阶段),以完成标记过程。 G1
收集器会短暂地停止应用线程,停止并发更新信息的写入,处理其中的少量信息,并标记所有在并发标 记开始时未被标记的存活对象。 这一阶段也执行某些额外的清理,如引用处理或者类卸载(class unloading
)。
对应的日志:
2022-01-10T10:42:59.664+0800: 1.173:
[GC remark 2022-01-10T10:42:59.664+0800: 1.173: [Finalize Marking, 0.0000447 secs]
2022-01-10T10:42:59.664+0800: 1.174: [GC ref-proc, 0.0000356 secs]
2022-01-10T10:42:59.664+0800: 1.174: [Unloading, 0.0004159 secs], 0.0018676 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
4.3.2.5 阶段 5: Cleanup(清理)
最后这个清理阶段为即将到来的转移阶段做准备,统计小堆块中所有存活的对象,并将小堆块进行排序,以提升GC
的效率。此阶段也为下一次标记执行必需的所有整理工作(house-keeping activities
):维护并发标记的内部状态。 所有不包含存活对象的小堆块在此阶段都被回收了。有一部分任务是并发的:例如空堆区的回收,还有大部分的存活率计算。此阶段也需要一个短暂的STW
暂停,才能不受应用线程的影响并完成作业。
这种STW
停顿的对应的日志如下:
2022-01-10T10:42:59.666+0800: 1.175: [GC cleanup 379M->379M(512M), 0.0002800 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
如果发现某些小堆块中只包含垃圾,则日志格式可能会有点不同
2022-01-10T10:42:58.906+0800: 0.416: [GC cleanup 369M->368M(512M), 0.0002913 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
2022-01-10T10:42:58.907+0800: 0.416: [GC concurrent-cleanup-start]
2022-01-10T10:42:58.907+0800: 0.416: [GC concurrent-cleanup-end, 0.0000089 secs]
如果没有看到对应的GC
日志,可以多跑几遍试试。毕竟GC
和内存分配属于运行时动态的,每次运行都可能有些不同。
标记周期一般只在碰到region
中一个存活对象都没有的时候,才会顺手处理一把,大多数情况下都不释放内存。示意图如下所示:
4.3.3 转移暂停: 混合模式(Evacuation Pause (mixed))
并发标记完成之后,G1
将执行一次混合收集(mixed collection
),不只清理年轻代,还将一部分老年代区域也加入到 collection set
中。 混合模式的转移暂停(Evacuation pause
)不一定紧跟并发标记阶段。在并发标记与混合转移暂停之间,很可能会存在多次 young
模式的转移暂停。
混合模式就是指这次GC
事件混合着处理年轻代和老年代的region
。这也是G1
等增量垃圾收集器的特色。而ZGC
等最新的垃圾收集器则不使用分代算法。 当然,以后可能还是会实现分代的,毕竟分代之后性能还会有提升。
混合模式下的日志,和纯年轻代模式相比还是有不同
2022-01-10T10:42:58.929+0800: 0.438: [GC pause (G1 Evacuation Pause) (mixed), 0.0023414 secs]
[Parallel Time: 2.0 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 438.5, Avg: 438.5, Max: 438.5, Diff: 0.0]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.0, Sum: 0.5]
// 这里=====
[Update RS (ms): Min: 0.2, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.7]
[Processed Buffers: Min: 0, Avg: 2.8, Max: 4, Diff: 4, Sum: 11]
[Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
// ======
[Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Object Copy (ms): Min: 1.7, Avg: 1.7, Max: 1.7, Diff: 0.0, Sum: 6.7]
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 4]
[GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
[GC Worker Total (ms): Min: 2.0, Avg: 2.0, Max: 2.0, Diff: 0.0, Sum: 8.1]
[GC Worker End (ms): Min: 440.6, Avg: 440.6, Max: 440.6, Diff: 0.0]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.1 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.1 ms]
[Eden: 16.0M(16.0M)->0.0B(21.0M) Survivors: 9216.0K->4096.0K Heap: 393.9M(512.0M)->331.6M(512.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
简单解读(部分概念和名称,可以参考上面,这里介绍不同的):
-
[Update RS (ms)
– 因为Remembered Sets
是并发处理的,必须确保在实际的垃圾收集之前,缓冲区中的card
得到处理。如果card
数量很多,则GC
并发线程的负载可能就会很高。可能的原因是修改的字段过多,或者CPU
资源受限。 -
Processed Buffers
– 各个worker
线程处理了多少个本地缓冲区(local buffer
)。 -
Scan RS (ms)
– 用了多长时间扫描来自RSet
的引用。 -
[Clear CT: 0.0 ms]
– 清理card table
中cards
的时间。清理工作只是简单地删除“脏”状态,此状态用来标识一个字段是否被更新的,供Remembered Sets
使用。 -
[Redirty Cards: 0.0 ms]
– 将card table
中适当的位置标记为dirty
所花费的时间。"适当的位置"是由GC
本身执行的堆内存改变所决定的,例如引用排队等。
4.3.4 Full GC (Allocation Failure)
G1
是一款自适应的增量垃圾收集器。一般来说,只有在内存严重不足的情况下才会发生Full GC
。 比如堆空间不足或者to-space
空间不足。 在前面的示例程序基础上,增加缓存对象的数量,即可模拟Full GC
。
日志如下:
2022-01-10T10:42:59.552+0800: 1.061: [Full GC (Allocation Failure) 419M->349M(512M), 0.0313367 secs]
[Eden: 0.0B(25.0M)->0.0B(25.0M)
Survivors: 0.0B->0.0B Heap: 419.6M(512.0M)->349.3M(512.0M)],
[Metaspace: 2608K->2608K(1056768K)]
[Times: user=0.03 sys=0.00, real=0.03 secs]
因为我们的堆内存空间很小,存活对象的数量也不多,所以这里看到的Full GC
暂停时间很短。
5. GC选择的经验总结
为了简洁我们省略了很多实现细节, 例如如何处理巨无霸对象(humongous objects)。 综合来看,G1
是JDK11
之前HotSpot JVM
中最先进的准产品级(production-ready
)垃圾收集器。重要的是, HotSpot
工程师的主要精力都放在不断改进G1
上面。在更新的JDK
版本中,将会带来更多强大的功能和优化。
可以看到,G1
作为CMS
的代替者出现,解决了 CMS
中的各种疑难问题,包括暂停时间的可预测性,并终结了堆内存的碎片化。对单业务延迟非常敏感的系统来说,如果CPU
资源不受限制,那么G1
可以说是 HotSpot
中最好的选择,特别是在最新版本的JVM
中。当然这种降低延迟的优化也不是没有代价的:由于额外的写屏障和守护线程,G1
的开销会更大。如果系统属于吞吐量优先型的,又或者CPU
持续占用100%
,而又不在乎单次GC
的暂停时间,那么CMS
是更好的选择。
总之: G1
适合大内存,需要较低延迟的场景。
选择正确的GC
算法,唯一可行的方式就是去尝试,并找出不合理的地方,一般性的指导原则:
- 如果系统考虑吞吐优先,
CPU
资源都用来最大程度处理业务,用Parallel GC
; - 如果系统考虑低延迟有限,每次
GC
时间尽量短,用CMS GC
; - 如果系统内存堆较大,同时希望整体来看平均
GC
时间可控,使用G1 GC
。
对于内存大小的考量:
- 一般
4G
以上,算是比较大,用G1
的性价比较高。 - 一般超过
8G
,比如16G-64G
内存,非常推荐使用G1 GC
。
最后讨论一个很多开发者经常忽视的问题,也是面试大厂常问的问题:
JDK8
的默认GC
是什么?
很多人或觉得是CMS
,甚至G1
,其实都不是。
看过这篇文章就知道答案是并行GC
注意,G1
成为JDK9
以后版本的默认GC
策略,同时, ParNew + SerialOld
这种组合不被支持。
欢迎一键三连~
有问题请留言,大家一起探讨学习
----------------------Talk is cheap, show me the code-----------------------