GC算法:实现
我们已经回顾了GC算法背后的核心概念,现在让我们转向到JVM中的特定实现。首先我们需要认识到的一个要点是,对于大多数JVM而言,需要两种不同的GC算法 - 一种用于清除年轻代,另一种用于清除老年代。
您可以指定JVM中实现的各种垃圾回收算法。如果未明确指定垃圾收集算法,则JVM将使用特定平台的默认算法。在本章中,将解释每种算法的工作原理。
以下列表是一个快速备忘清单,可以让您自己快速掌握哪种算法组合。需要注意的是,以下清单适用于Java 8,对于较旧的Java版本,可用的组合可能略有不同:
年轻代 | 老年代 | JVM选项 |
---|---|---|
Incremental | Incremental | -Xincgc |
Serial | Serial | -XX:+ UseSerialGC |
Parallel Scavenge | Serial | -XX:+ UseParallelGC -XX:-UseParallelOldGC |
Parallel New | Serial | N / A |
Serial | Parallel Old | N / A |
Parallel Scavenge | Parallel Old | -XX:+ UseParallelGC -XX:+ UseParallelOldGC |
Parallel New | Parallel Old | N / A |
Serial | CMS | -XX:-UseParNewGC -XX:+ UseConcMarkSweepGC |
Parallel Scavenge | CMS | N / A |
Parallel New | CMS | -XX:+ UseParNewGC -XX:+ UseConcMarkSweepGC |
G1 | -XX:+ UseG1GC |
如果你觉得上面这些组合看起来很复杂,千万不用担心。实际上,现在jvm支持的主要有四种组合。其它的要么被弃用,要么不被支持,甚至现实实现是不切实际的。因此,在接下来的章节中,我们将介绍以下组合的工作原理:
- 老年代和年轻代都是Serial GC
- 老年代和年轻代都是Parallel GC
- 年轻代Parallel New+老年代CMS
- G1,包括Young和Old的垃圾收集
串行GC(Serial GC)
这个垃圾收集器年轻代是用标记-复制算法,老年代用标记-清除-压缩算法。顾名思义 - 这两个收集器都是单线程收集器,无法并行化手头的任务。两个收集器还会触发stop-the-world暂停,停止所有应用程序线程。
因此,该GC算法不能利用现代硬件中常见的多个CPU核的优势。与可用内核数量无关,JVM在垃圾回收期间只使用一个内核。
通过在JVM启动脚本中指定单个参数来为Young和Old Generation启用此收集器:
java -XX:+ UseSerialGC com.mypackages.MyExecutableClass
在JVM堆大小为几百M并且是单核cpu的时候建议启用该算法。对于大多数服务器端部署,这是一种罕见的组合。大多数服务器端部署都是在具有多个内核的平台上完成的,这实际上意味着通过选择串行GC,对gc使用系统资源设置了限制。这会导致资源空闲,本来这些资源可以用于减少延迟或增加吞吐量。
现在让我们回顾一下使用Serial GC时垃圾收集器日志的详情以及可以从那里获得的有用信息。为此,我们使用以下参数打开了JVM上的GC日志记录:
-XX:+ PrintGCDetails -XX:+ PrintGCDateStamps -XX:+ PrintGCTimeStamps
结果输出类似于以下内容:
2015-05-26T14:45:37.987-0200: 151.126: [GC (Allocation Failure) 151.126: [DefNew: 629119K->69888K(629120K), 0.0584157 secs] 1619346K->1273247K(2027264K), 0.0585007 secs] [Times: user=0.06 sys=0.00, real=0.06 secs]
2015-05-26T14:45:59.690-0200: 172.829: [GC (Allocation Failure) 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs]172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs] 1832479K->755802K(2027264K), [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs] [Times: user=0.18 sys=0.00, real=0.18 secs]
GC日志中的这种简短片段公开了大量有关JVM内部发生的活动的信息。事实上,在这个片段中有两个垃圾收集活动正在进行,其中一个清除年轻代,另一个清除整个堆。让我们首先分析年轻代发生的GC。
Minor GC
以下代码段包含有关清理年轻代的GC事件的信息:
2015-05-26T14:45:37.987-0200: 151.126: [GC (Allocation Failure) 151.126: [DefNew: 629119K->69888K(629120K), 0.0584157 secs] 1619346K->1273247K(2027264K), 0.0585007 secs] [Times: user=0.06 sys=0.00, real=0.06 secs]
-
2015-05-26T14:45:37.987-0200 - GC事件开始的时间。
-
151.126 - GC事件开始的时间,相对于JVM启动时间。以秒为单位。
-
GC - 用于区分Minor和Full GC的标志。这次它表明这是Minor GC。
-
Allocation Failure - 触发垃圾收集的原因。在这种情况下,由于数据结构不适合Young Generation中的任何区域,因此触发GC。
-
DefNew - 使用的垃圾收集器的名称。这个神秘的名字代表用于清理Young一代的单线程标记复制stop-the-world垃圾收集器。
-
629119K-> 69888K - 收集前后年轻代的使用情况。
-
(629120K) - 年轻代的总大小。
-
1619346K-> 1273247K - 收集前后使用的总堆数。
-
(2027264K) - 可用堆总量。
-
0.0585007秒 - GC事件的持续时间(以秒为单位)。
-
[时间:user=0.06 sys=0.00, real=0.06 secs]
- GC事件的持续时间,以不同类别衡量:
- user - 此集合期间垃圾收集器线程消耗的总CPU时间
- sys - 在OS调用或等待系统事件中花费的时间
- real - 您的应用程序停止的时钟时间。由于串行垃圾收集器始终仅使用单个线程,因此实时等于用户和系统时间的总和。
从上面的代码片段中我们可以准确理解GC事件进行期间JVM内部消耗的情况。在此垃圾收集发生之前,堆使用总计为1,619,346K。除此之外,年轻代消耗了629,119K。由此我们可以计算出老年代的使用量等于990,227K。
一个更重要的结论隐藏在数字中,垃圾收集后,年轻代使用量减少了559,231K,但总堆使用量仅减少了346,099K。由此我们可以再次得出213,132K大小的对象从年轻代晋升到了老年代。
此GC事件还通过以下图片进行说明,这些图片显示GC启动前和完成后的内存使用情况:
Full GC
在了解了Minor GC之后,让我们来分析日志中的第二个GC事件:
2015-05-26T14:45:59.690-0200: 172.829: [GC (Allocation Failure) 172.829: [DefNew: 629120K->629120K(629120K), 0.0000372 secs]172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs] 1832479K->755802K(2027264K), [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs] [Times: user=0.18 sys=0.00, real=0.18 secs]
-
2015-05-26T14:45:59.690-0200 - GC事件开始的时间。
-
172.829 - 相对于JVM启动时间,GC事件开始的时间。以秒为单位测量。
-
[DefNew:629120K-> 629120K(629120K),0.0000372秒 - 与前一个示例类似,由于分配失败,在此事件期间发生了年轻代中的Minor GC。对于full GC,DefNew收集器像Minor GC中一样运行,它将年轻代的使用量从629120K减少到0。此收集耗时0.0000372秒。
-
Tenured - 用于清理老年代的垃圾收集器的名称。名称Tenured表示正在使用单线程的mark-sweep-compact垃圾收集器,该收集器会触发stop-the-world。
-
1203359K-> 755802K - 在收集发生之前和之后老年代的使用情况。
-
(1398144K) - 老年代的总容量。
-
0.1855567秒 - 清理老年代的时间。
-
1832479K-> 755802K - 在收集年轻代和老年代之前和之后整个堆的使用情况。
-
(2027264K) - JVM可用的堆总容量。
-
[Metaspace:6741K-> 6741K(1056768K)] - Metaspace收集的信息类似。如图所示,在活动期间Metaspace没有发生垃圾收集。
-
[Times: user=0.18 sys=0.00, real=0.18 secs]
- GC事件的持续时间,以不同类别衡量:
- user - 此垃圾收集期间垃圾收集器线程消耗的总CPU时间
- sys - 在OS调用或等待系统事件中花费的时间
- real - 您的应用程序停止的时钟时间。由于串行垃圾收集器始终仅使用单个线程,因此时钟时间等于用户和系统时间的总和。
与Minor GC的区别很明显 - 除了年轻代之外,在此GC事件执行期间,老年代和Metaspace也被清理干净。事件之前和之后的内存布局看起来如下图所示:
并行GC
这种垃圾收集器在年轻代中使用mark-copy算法,在老年代中使用mark-sweep-compact算法。年轻代和老年代垃圾收集都会触发stop-the-world事件,停止所有应用程序线程来执行垃圾收集。两个收集器都使用多个线程运行标记和复制/压缩阶段,因此名称为“Parallel”。使用这种算法,可以大大减少收集时间。
垃圾收集期间使用的线程数可通过命令行参数 -XX:ParallelGCThreads = NNN进行配置 。默认值等于计算机中的核数。
并行GC垃圾收集器的选择可以通过组合以下JVM脚本启动参数来指定:
java -XX:+ UseParallelGC com.mypackages.MyExecutableClass
java -XX:+ UseParallelOldGC com.mypackages.MyExecutableClass
java -XX:+ UseParallelGC -XX:+ UseParallelOldGC com.mypackages.MyExecutableClass
如果您的主要目标是提高吞吐量,并行垃圾收集器适用于多核服务器。由于能更有效地使用系统资源,因此实现了更高的吞吐量:
- 在收集过程中,所有核都在并行清理垃圾,从而缩短暂停时间
- 在垃圾收集间歇,垃圾收集器没有消耗任何系统资源
另一方面,由于垃圾收集的所有阶段都必须在没有任何中断的情况下发生,因此这些收集器仍然容易受到长时间暂停的影响,在此期间应用程序线程将被停止。因此,如果延迟是您的主要目标,您应该考虑垃圾收集器的下一个组合。
现在让我们回顾一下使用Parallel GC时垃圾收集器的日志以及可以从那里获得的有用信息。为此,让我们再看一下垃圾收集器日志,它们展示了一次minor GC和一次major GC:
2015-05-26T14:27:40.915-0200: 116.115: [GC (Allocation Failure) [PSYoungGen: 2694440K->1305132K(2796544K)] 9556775K->8438926K(11185152K), 0.2406675 secs] [Times: user=1.77 sys=0.01, real=0.24 secs]
2015-05-26T14:27:41.155-0200: 116.356: [Full GC (Ergonomics) [PSYoungGen: 1305132K->0K(2796544K)] [ParOldGen: 7133794K->6597672K(8388608K)] 8438926K->6597672K(11185152K), [Metaspace: 6745K->6745K(1056768K)], 0.9158801 secs] [Times: user=4.49 sys=0.64, real=0.92 secs]
Minor GC
这两个事件中的第一个表明在年轻代中发生了GC事件:
2015-05-26T14:27:40.915-0200: 116.115: [GC (Allocation Failure) [PSYoungGen: 2694440K->1305132K(2796544K)] 9556775K->8438926K(11185152K), 0.2406675 secs] [Times: user=1.77 sys=0.01, real=0.24 secs]
-
2015-05-26T14:27:40.915-0200 - GC事件开始的时间。
-
116.115 - 相对于JVM启动时间,GC事件开始的时间。以秒为单位测量。
-
GC - 用于区分Minor和Full GC的标志。”GC”表明这是Minor GC。
-
Allocation Failure - 触发垃圾收集的原因。在这种情况下,由于数据结构不适合年轻代中的任何区域,因此触发GC。
-
PSYoungGen - 使用的垃圾收集器的名称,表示用于清理年轻代的并行标记复制带有stop-the-world安全点的垃圾收集器。
-
2694440K-> 1305132K - 收集前后年轻代的使用情况
-
(2796544K) - 年轻代的总容量
-
9556775K-> 8438926K - 收集前后总堆的使用情况
-
(11185152K) - 可用堆总量
-
0.2406675 secs - GC事件的持续时间(以秒为单位)
-
[Times: user=1.77 sys=0.01, real=0.24 secs]
- GC事件的持续时间,以不同类别衡量:
- user - 此垃圾收集期间垃圾收集器线程消耗的总CPU时间
- sys - 在OS调用或等待系统事件中花费的时间
- real - 您的应用程序停止的时钟时间。对于并行GC,此数字应接近(用户时间+系统时间)除以垃圾收集器使用的线程数。在这种特殊情况下,使用了8个线程。请注意,由于某些活动无法并行化,因此它总是超过一定比例的比率。
因此,简而言之,收集前的总堆消耗为9,556,775K。年轻代中有2,694,440K。这意味着使用的老年代是6,862,335K。收集后年轻代使用量减少了1,389,308K,但总堆使用量仅减少了1,117,849K。这意味着271,459K对象从年轻代晋升到老年代。
Full GC
在了解了Parallel GC如何清理年轻代之后,我们准备通过分析GC日志中的下一个片段来了解该垃圾收集器如何清理整个堆:
2015-05-26T14:27:41.155-0200: 116.356: [Full GC (Ergonomics) [PSYoungGen: 1305132K->0K(2796544K)] [ParOldGen: 7133794K->6597672K(8388608K)] 8438926K->6597672K(11185152K), [Metaspace: 6745K->6745K(1056768K)], 0.9158801 secs] [Times: user=4.49 sys=0.64, real=0.92 secs]
-
2015-05-26T14:27:41.155-0200 - GC事件开始的时间
-
116.356 - 相对于JVM启动时间,GC事件开始的时间。以秒为单位测量。在这种情况下,我们可以看到事件在上一次Minor GC完成后立即开始。
-
Full GC - 标记表示事件这是Full GC,它会清理年轻代和老年代。
-
Ergonomics - GC发生的原因。这表明JVM内部的Ergonomics决定了这是收集垃圾的正确时机。
-
[PSYoungGen:1305132K-> 0K(2796544K)] - 与前面的例子类似,表示这是一个名为“PSYoungGen”的收集器,该收集器使用mark-copy算法并且会触发stop-the-world用于清理年轻代。年轻代的使用从1305132K缩减到0,这是Full GC的典型结果。
-
ParOldGen - 用于清理老年代的收集器的类型。在这种情况下,使用名为ParOldGen的收集器,该收集器使用mark-sweep-compact算法,并且会触发stop-the-world。
-
7133794K-> 6597672K - 在收集之前和之后老年代使用情况
-
(8388608K) - 老年代的总容量
-
8438926K-> 6597672K - 在收集之前和之后整个堆使用情况。
-
(11185152K) - 可用总堆容量
-
[Metaspace:6745K-> 6745K(1056768K)] - 有关Metaspace地区的类似信息。我们可以看到,在此次活动期间,Metaspace中没有垃圾收集。
-
0.9158801秒 - GC事件的持续时间(以秒为单位)
-
[Times: user=4.49 sys=0.64, real=0.92 secs]
- GC事件的持续时间,以不同类别衡量:
- user - 此集合期间垃圾收集器线程消耗的总CPU时间
- sys - 在OS调用或等待系统事件中花费的时间
- real - 您的应用程序停止的时钟时间。对于并行GC,此数字应接近(用户时间+系统时间)除以垃圾收集器使用的线程数。在这种特殊情况下,使用了8个线程。请注意,由于某些活动无法并行化,因此它总是超过一定比例的比率。
同样的,与Minor GC的区别很明显 - 除了年轻代之外,在此GC事件期间,老年代和Metaspace也被清理干净。事件之前和之后的内存布局看起来如下图所示:
并发标记和清除(CMS)
这个垃圾收集器的官方名称是“Mostly Concurrent Mark and Sweep Garbage Collector”。在年轻代中使用并行的带有stop-the-world安全点的mark-copy算法,而在老年代中用的是大多数阶段都是处于并发执行的mark-sweep算法(mark-and-sweep阶段的大部分工作都与应用程序并发完成)。
这个垃圾收集器的目的是避免在老年代收集时长时间停顿。它通过两种方式实现这一目标。首先,它不会压缩老年代,而是使用空闲列表来管理回收的空间。其次,mark-and-sweep阶段的大部分工作都与应用程序并发完成。这意味着垃圾收集不会显式停止应用程序线程来执行这些阶段。但是需要注意的是,它仍然会与应用程序线程竞争CPU时间。默认情况下,此GC算法使用的线程数等于计算机物理核数的1/4。
可以通过在命令行上指定以下选项来选择此垃圾收集器:
java -XX:+ UseConcMarkSweepGC com.mypackages.MyExecutableClass
如果您的主要目标是延迟,这种组合在多核机器上是一个不错的选择。减少单次GC暂停的持续时间会直接影响用户最终感知应用程序响应的时间,从而使他们感觉应用程序响应更快。由于大多数时候GC至少消耗了一些CPU资源而没有执行应用程序的代码,因此CMS通常比并行GC的吞吐量更差。
与之前的GC算法一样,现在让我们来分析一下minor GC和major GC::
2015-05-26T16:23:07.219-0200: 64.322: [GC (Allocation Failure) 64.322: [ParNew: 613404K->68068K(613440K), 0.1020465 secs] 10885349K->10880154K(12514816K), 0.1021309 secs] [Times: user=0.78 sys=0.01, real=0.11 secs]
2015-05-26T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]
2015-05-26T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559: [weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
Minor GC
日志中的第一个GC事件表示系统执行了一次清理年轻代空间的minor GC。让我们分析一下这个收集器组合在这方面的表现:
2015-05-26T16:23:07.219-0200: 64.322: [GC (Allocation Failure) 64.322: [ParNew: 613404K->68068K(613440K), 0.1020465 secs] 10885349K->10880154K(12514816K), 0.1021309 secs] [Times: user=0.78 sys=0.01, real=0.11 secs]
-
2015-05-26T16:23:07.219-0200 - GC事件开始的时间。
-
64.322 - GC事件启动的时间,相对于JVM启动时间。以秒为单位测量。
-
GC - 用于区分Minor和Full GC的标志。”GC”表明是Minor GC。
-
Allocation Failure - 垃圾收集触发的原因。在这种情况下,由于在年轻代中找不到合适大小的内存来存储对象,因此触发GC。
-
ParNew - 使用的收集器的名称,”ParNew “收集器在年轻代中使用的并行标记复制算法,该算法会触发stop-the-world安全点,该收集器与老年代中的Concurrent Mark&Sweep垃圾收集器一起使用。
-
613404K-> 68068K - 收集前后年轻代的使用情况。
-
(613440K) - 年轻代的总容量。
-
0.1020465秒 - 最终清理阶段的持续时间。
-
10885349K-> 10880154K - 收集前后使用的总堆数。
-
(12514816K) - 可用堆总量。
-
0.1021309秒 - 垃圾收集器在年轻代中标记和复制活动对象所花费的时间。这包括与ConcurrentMarkSweep收集器的通信开销,促进对老年代足够老的对象以及在垃圾收集周期结束时的一些最终清理。
-
[Times: user=0.78 sys=0.01, real=0.11 secs]
- GC事件的持续时间,以不同类别衡量:
- user - 此集合期间垃圾收集器线程消耗的总CPU时间
- sys - 在OS调用或等待系统事件中花费的时间
- real - 您的应用程序停止的时钟时间。对于并行GC,此数字应接近(用户时间+系统时间)除以垃圾收集器使用的线程数。在这种特殊情况下,使用了8个线程。请注意,由于某些活动无法并行化,因此它总是超过一定比例的比率。
从上面我们可以看到,在收集之前,使用的总堆数为10,885,349K,使用的年轻代的空间为613,404K。这意味着老年代的空间为10,271,945K。收集后,年轻代使用量减少了545,336K,但总堆使用量仅减少了5,195K。这意味着540,141K从年轻代晋升到老年代。
Full GC
现在,就像您已经习惯于阅读GC日志一样,本节将为日志中的下一个垃圾收集事件引入完全不同的格式。随后的冗长输出包括老年代中大多数并发垃圾收集的所有不同阶段。我们将逐一分析它们,但在这种情况下,我们将分阶段分析日志内容而不是整个事件日志一起分析,以便更简洁地表示。但首先让我们来回顾一下,CMS收集器的整个事件.如下所示:
2015-05-26T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 0.035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]
2015-05-26T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559: [weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
事实上,年轻代的Minor GC可以与老年代的垃圾收集一起进行。在这种情况下,下面看到的major GC记录将与前一章中涉及的minor GC事件交错。
阶段1:初始标记。这是CMS期间的两个stop-the-world事件之一。此阶段的目标是将老年代中的所有GC根对象或者是被年轻代对象引用的对象标记出来。后者很重要,因为老年代是分开收集的。
2015-05-26T16:23:07.321-0200: 64.425: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10812086K(11901376K)] 10887844K(12514816K), 0.0001997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
- 2015-05-26T16:23:07.321-0200:64.42 - GC事件开始的时间,包括时钟时间和相对于JVM启动时的时间。
- CMS Initial Mark - 集合的阶段 - 此时“初始标记” - 即收集所有GC根。
- 10812086K - 目前已使用的老年代内存。
- (11901376K) - 老年代的总可用内存。
- 10887844K - 当前使用的堆内存
- (12514816K) - 可用堆总量
- [Times: user=0.00 sys=0.00, real=0.00 secs]- 阶段的持续时间,也分用户,系统和实时测量。
第2阶段:并发标记。在此阶段,垃圾收集器遍历老年代对象并标记所有活动对象,从上一阶段“初始标记”中找到的GC根开始。顾名思义,“并发标记”阶段与您的应用程序同时运行,并且不会停止应用程序线程。请注意,并非老年代中的所有活动对象都可能被标记,因为应用程序在标记期间正在改变引用。
在图示中,标记线程在标记的同时,一个引用正在移出对当前对象的引用。
2015-05-26T16:23:07.321-0200: 64.425: [CMS-concurrent-mark-start]
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-mark: 035/0.035 secs] [Times: user=0.07 sys=0.00, real=0.03 secs]
- CMS-concurrent-mark - 集合的阶段 - 当前是“并发标记” 阶段- 遍历老年代并标记所有活动对象。
- 035/0.035 secs] - 阶段的持续时间,相应地显示经过时间和挂钟时间。
- [Times: user=0.07 sys=0.00, real=0.03 secs]- “Times”部分对并发阶段的意义不大,因为它是从并发标记的开始测量的,并且不仅仅包括为并发标记完成的工作。
阶段3:并发预清除。这又是一个并发阶段,与应用程序线程并行运行,而不是暂停它们。虽然前一阶段与应用程序同时运行,但一些引用已更改。只要发生这种情况,JVM就会将包含已修改对象的堆区域(称为“Card”)标记为“dirty”(这称为卡标记)。
在预清除阶段,会考虑这些”dirty”对象,并且还会标记从它们可以到达的对象。完成后清除这些”cards “。
此外,还进行了一些必要的管理和最终标记阶段的准备工作。
2015-05-26T16:23:07.357-0200: 64.460: [CMS-concurrent-preclean-start]
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-preclean: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
- CMS-concurrent-preclean - 集合的阶段 - 此时“并发预清除” - 考虑在前一个标记阶段中被更改的引用。
- 0.016 / 0.016秒 - 相应的持续时间,相应地显示执行时间和挂钟时间。
- [Times: user=0.02 sys=0.00, real=0.02 secs] - “Times”部分对于并发阶段没有意义,因为它是从并发标记的开始测量的,并且不仅仅包括为并发标记完成的工作。
阶段4:可中断的并发预清除。同样,一个并没有停止应用程序线程的并发阶段。这个阶段尝试尽可能多地减少会触发stop-the-world 最终清除阶段的工作。这个阶段的确切持续时间取决于许多因素,因为它迭代做同样的事情,直到满足一个中断条件(例如迭代次数,完成的有用工作量,经过的挂钟时间等)。
2015-05-26T16:23:07.373-0200: 64.476: [CMS-concurrent-abortable-preclean-start]
2015-05-26T16:23:08.446-0200: 65.550: [CMS-concurrent-abortable-preclean: 0.167/1.074 secs] [Times: user=0.20 sys=0.00, real=1.07 secs]
- CMS-concurrent-abortable-preclean - 当前是垃圾收集的“Concurrent Abortable Preclean”系列阶段
- 0.167/1.074 secs - 持续时间,分别显示执行时间和挂钟时间。有趣的是,报告的用户时间比时钟时间小很多。通常我们已经看到实时时间少于用户时间,这意味着一些工作是并行完成的,因此经过的时钟时间少于使用CPU的时间。在这里,我们有一些工作 - 对于0.167秒的CPU时间,垃圾收集器线程正在做很多等待。基本上,他们试图尽可能长时间地避开STW暂停。默认情况下,此阶段可能持续最多5秒。
- [Times: user=0.20 sys=0.00, real=1.07 secs]- “Times”部分对并发阶段的意义不大,因为它是从并发标记的开始算的,所以不仅仅包括并发工作完成的时间。
此阶段可能会显着影响即将到来的stop-the-world的停顿持续时间,并且具有相当多的特殊配置选项和失败模式。
第5阶段:最终标记。这是垃圾收集期间的第二个也是最后一个stop-the-world阶段。这个stop-the-world阶段的目标是最终确定标记老年代中的所有活动对象。由于之前的预扫描阶段是并发的,他们可能无法跟上应用程序对对象的更改速度。完成标记需要stop-the-world停顿。
通常,当收集器尽可能在老年代为空时,CMS才尝试运行最终标记阶段,以消除连续的发生几个stop-the-world的可能性。
此事件看起来比以前的阶段更复杂:
2015-05-26T16:23:08.447-0200: 65.550: [GC (CMS Final Remark) [YG occupancy: 387920 K (613440 K)]65.550: [Rescan (parallel) , 0.0085125 secs]65.559: [weak refs processing, 0.0000243 secs]65.559: [class unloading, 0.0013120 secs]65.560: [scrub symbol table, 0.0008345 secs]65.561: [scrub string table, 0.0001759 secs][1 CMS-remark: 10812086K(11901376K)] 11200006K(12514816K), 0.0110730 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start]
2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start]
2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
- 2015-05-26T16:23:08.447-0200:65.550 - GC事件开始的时间,包括时钟时间和相对于JVM启动时的时间。
- CMS Final Remark- 垃圾收集的阶段 - 当前是“Final Remark”阶段 - 标记老年代中的所有活动对象,包括在先前并发标记阶段创建/修改的引用。
- YG occupancy:387920 K(613440 K) - 年轻代的现有占用率和容量。
- Rescan (parallel),0.0085125 secs] - “Rescan ”在应用程序停止时完成活动对象的标记。在这种情况下,重新扫描是并行完成的,耗时0.0085125秒。
- weak refs processing,0.0000243 secs] 65.559 - 处理弱引用的阶段以及阶段的持续时间和时间戳。
- class unloading,0.0013120 secs] 65.560 - 卸载未使用类的阶段,阶段的持续时间和时间戳。
- scrub string table,0.0001759 secs - 正在清理符号和字符串表的最终子阶段,它们分别包含类级元数据和内部化字符串。还包括暂停的时钟时间。
- 10812086K(11901376K) - 阶段后的老年代的占有率和容量。
- 11200006K(12514816K) - 使用容量和总堆的容量。
- 0.0110730 secs - 阶段的持续时间。
- [Times: user=0.06 sys=0.00, real=0.01 secs]- 暂停的持续时间,以用户,系统和实时类别衡量。
在这个标记阶段之后,老年代中的所有活动对象都被标记,现在垃圾收集器将通过扫描老年代来回收所有未使用的对象:
阶段6:并发清除。与应用程序同时执行,无需停止应用程序线程。该阶段的目的是删除未使用的对象并回收它们占用的空间以备将来使用。
2015-05-26T16:23:08.458-0200: 65.561: [CMS-concurrent-sweep-start] 2015-05-26T16:23:08.485-0200: 65.588: [CMS-concurrent-sweep: 0.027/0.027 secs] [[Times: user=0.03 sys=0.00, real=0.03 secs]
- CMS-concurrent-sweep - 当前是垃圾收集的“并发扫描”的阶段,清除未被标记并且未被使用的对象以回收空间。
- 0.027 / 0.027 secs - 阶段的持续时间,相应地显示执行时间和挂钟时间。
- [Times: user=0.03 sys=0.00, real=0.03 secs] - “Times”部分对并发阶段的意义不大,因为它是从并发标记的开始测量的,并且不仅仅包括为并发标记完成的工作。
阶段7:并发重置。并发的执行阶段,重置CMS算法的内部数据结构并为下一个周期做好准备。
2015-05-26T16:23:08.485-0200: 65.589: [CMS-concurrent-reset-start] 2015-05-26T16:23:08.497-0200: 65.601: [CMS-concurrent-reset: 0.012/0.012 secs] [[Times: user=0.01 sys=0.00, real=0.01 secs]
- CMS-concurrent-reset - 垃圾收集的阶段 - 当前为“CMS-concurrent-reset-start” 阶段- 即重置CMS算法的内部数据结构并准备下一个垃圾收集。
- 0.012 / 0.012 secs- 相应的持续时间,测量执行时间和挂钟时间。
- [Times: user=0.01 sys=0.00, real=0.01 secs] - “Times”部分对并发阶段的意义不大,因为它是从并发标记的开始测量的,并且不仅仅包括为并发工作完成的工作标记。
总而言之,CMS垃圾收集器通过将大量工作放到不需要应用程序停止的并发线程来做,用来减少暂停持续时间。然而,它也有一些缺点,其中最值得注意的是老年代碎片,在某些情况下暂停持续时间缺乏可预测性,特别是在大堆上。
G1 收集器
G1的一个关键设计目标是,使得stop-the-world暂停的持续时间和分布变得可预测和可配置。实际上,Garbage-First是一个软实时垃圾收集器,这意味着您可以为其设置特定的性能目标。您可以在任何给定的y毫秒长时间范围内请求stop-the-world暂停不超过x毫秒,例如在任何给定的时间内不超过5毫秒。Garbage-First GC将尽最大努力以高概率实现这一目标(但不确定,这将是难以实时的)。
G1做了许多工作来实现这一目标。首先,堆不必分成连续的老年代和年轻代。相反,堆被分成多个(通常是2048个)较小的堆区域,可以容纳对象。每个区域可以是伊甸园区域,幸存者区域或老年代区域。所有伊甸园和幸存者区的逻辑组成的单元就是年轻代,所有老年代区域组合起来就是老年代:
这允许GC避免一次收集整个堆,而是逐步地处理问题:一次只考虑一个区域的子集,称为收集集。在每次暂停期间收集所有年轻代区域,但也可以包括一些老年代区域:
G1的另一个新颖之处在于,在并发阶段,它估计每个区域包含的实时数据量。这用于构建集合集:首先收集包含最多垃圾对象的区域。因此名称:垃圾优先收集。
要在启用G1收集器的情况下运行JVM,请用下面的命令行启动你的应用程序
java -XX:+ UseG1GC com.mypackages.MyExecutableClass
疏散暂停:Fully Young
在应用程序生命周期的开始阶段,它最初在完全年轻模式下运行。当年轻代填满时,应用程序线程被暂停,年轻代区域内的实时数据被复制到Survivor区域,或任何由此成为Survivor区域的自由区域。
复制这些对象的过程称为疏散的过程,它的工作方式与我们之前看到的其他年轻代收集器的工作方式非常相似。疏散暂停的完整日志相当大,因此,为了简单起见,我们将省去一些日志。此外,由于日志记录的大小原因,并行阶段详细信息和“其他”阶段详细信息将独立讲解:
0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs] [Parallel Time: 13.9 ms, GC Workers: 8] … [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.1 ms] [Other: 0.4 ms] … [Eden: 24.0M(24.0M)->0.0B(13.0M) Survivors: 0.0B->3072.0K Heap: 24.0M(256.0M)->21.9M(256.0M)] [Times: user=0.04 sys=0.04, real=0.02 secs]
-
0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs]- G1暂停仅清除(年轻)区域。JVM启动后134ms开始暂停,暂停时间持续时间为挂钟时间0.0144秒。
-
[Parallel Time: 13.9 ms, GC Workers: 8]- 表示在13.9毫秒(实时)内,8个线程并行执行了以下活动
-
… - 为简洁起见,请参阅以下部分了解详细信息。
-
[Code Root Fixup: 0.0 ms] - 释放用于管理并行活动的数据结构。应始终接近零。这是按顺序完成的。
-
[Code Root Purge: 0.0 ms] - 清理更多数据结构,也应该非常快,但不一定接近零。这是按顺序完成的。
-
[Other: 0.4 ms] - 其他活动,其中许多活动也是并行化的
-
… - 有关详细信息,请参阅以下部分
-
[Eden: 24.0M(24.0M)->0.0B(13.0M) - 暂停前后伊甸园区域的使用量和总容量
-
Survivors: 0.0B->3072.0K - 暂停前后幸存者区域使用的空间
-
Heap: 24.0M(256.0M)->21.9M(256.0M)]- 暂停前后的堆使用量和总容量。
-
[Times: user=0.04 sys=0.04, real=0.02 secs]
- GC事件的持续时间,以不同类别衡量:
- user - 垃圾收集期间垃圾收集器线程消耗的总CPU时间
- sys - 在OS调用或等待系统事件中花费的时间
- real - 您的应用程序停止的时钟时间。通过GC期间的可并行化活动,此数字理想化情况下接近(用户时间+系统时间)除以垃圾收集器使用的线程数。当前情况,使用了8个线程。请注意,由于某些活动无法并行化,因此它总是超过一定比例的比率。
大多数繁重工作都是由多个专用GC工作线程完成的。他们的活动在日志如下描述:
[Parallel Time: 13.9 ms, GC Workers: 8] [GC Worker Start (ms): Min: 134.0, Avg: 134.1, Max: 134.1, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 1.2] [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.2, Diff: 0.2, Sum: 0.2] [Object Copy (ms): Min: 10.8, Avg: 12.1, Max: 12.6, Diff: 1.9, Sum: 96.5] [Termination (ms): Min: 0.8, Avg: 1.5, Max: 2.8, Diff: 1.9, Sum: 12.2] [Termination Attempts: Min: 173, Avg: 293.2, Max: 362, Diff: 189, Sum: 2346] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] GC Worker Total (ms): Min: 13.7, Avg: 13.8, Max: 13.8, Diff: 0.1, Sum: 110.2] [GC Worker End (ms): Min: 147.8, Avg: 147.8, Max: 147.8, Diff: 0.0]
- [Parallel Time: 13.9 ms, GC Workers: 8] - 表示在13.9 ms(时钟时间)下,8个并行线程执行了以下活动
- [GC Worker Start(ms) - 工作线程开始工作的时间点,与暂停开始时的时间戳匹配。如果Min和Max差别很大,那么可能表明使用了太多线程或者机器上的其他进程正在从JVM内部的垃圾收集进程中抢夺CPU时间.
- [Ext Root Scanning(ms) - 扫描外部(非堆)根,例如类加载器,JNI引用,JVM系统根等所花费的时间。显示已用时间,“Sum”是CPU时间
- [Code Root Scanning (ms) - 扫描来自实际代码的根所花费的时间:本地变量等。
- [Object Copy (ms) - 将实时对象复制远离收集区域所需的时间。
- [Termination (ms) - 工作线程确保可以安全停止并且没有更多工作要做,然后实际终止所需的时间
- [Termination Attempts - 工作线程尝试和终止的尝试次数。如果工作线程发现实际上还有更多的工作要做,那么尝试终止就会失败,现在终止还为时过早。
- [GC Worker Other(ms) - 其他小活动,不在日志中的单独说明。
- GC Worker Total(ms) - 工作线程总共工作了多长时间
- [GC Worker End(ms) - 工作线程完成工作的时间戳。通常它们应该大致相等,否则它可能表示系统运行的线程太多.
此外,在疏散暂停期间还会执行一些其他活动。我们将仅在本节中介绍其中的一部分。其余的将在稍后介绍。
[Other: 0.4 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.2 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms]
- [Other: 0.4 ms] - 其他活动,其中许多也是并行化的
- [Ref Proc:0.2 ms] - 处理非强引用所花费的时间:清除它们或确定不需要清除。
- [Ref Enq:0.0 ms] - 将剩余的非强引用排入适当的ReferenceQueue所花费的时间
- [Free CSet:0.0 ms] - 返回垃圾收集集中已释放区域所需的时间,以便它们可用于新对象的内存分配。
并发标记
G1收集器建立在上一节CMS的许多概念之上,因此在继续之前确保您对它有足够的了解。即使它在很多方面有所不同,但并发标记的目标非常相似。G1并发标记使用Snapshot-At-The-Beginning方法,标记在标记周期开始时生效的所有对象,即使它们同时变为垃圾对象。有关哪些对象的实时信息允许为每个区域建立活跃度统计数据,以便之后可以有效地选择收集集。
然后,此信息用于在老年代区域中执行垃圾收集。如果标记确定区域仅包含垃圾对象,或者在包含垃圾和活动对象的老年代的stop-the-world疏散暂停期间,它可以完全同时发生。
并发标记阶段在堆的总占用率足够大时开始。默认情况下,它是45%,但可以通过InitiatingHeapOccupancyPercentJVM选项进行更改。与CMS类似,G1中的并发标记由多个阶段组成,其中一些完全并发,其中一些阶段需要停止应用程序线程。
阶段1:初始标记。此阶段标记可从GC根直接访问的所有对象。在CMS中,它需要一个单独的stop-the-world暂停,但在G1中它通常是在疏散暂停时捎带的,因此其开销很小。您可以通过疏散暂停的GC日志中第一行的“(initial-mark)”看到它:
1.631: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0062656 secs]
阶段2:根区域扫描。此阶段标记从根区域可到达的所有活动对象,即那些非空的并且我们可能最终必须在标记周期内收集的活动对象。由于在并发标记期间改变引用会引起麻烦,因此必须在下一个疏散暂停开始之前完成此阶段。如果必须提前启动,它将请求提前中止根区域扫描,然后等待它完成。在当前的实现中,根区域是幸存区域:它们是年轻代区域,肯定会在下一个疏散暂停中收集。
1.362: [GC concurrent-root-region-scan-start]
1.364: [GC concurrent-root-region-scan-end, 0.0028513 secs]
阶段3.并发标记。这个阶段非常类似于CMS:它只是遍历对象图并在特殊位图中标记访问过的对象。为了确保满足开头快照的语义,G1 GC要求应用程序线程对对象图进行的所有并发更新都会将先前的引用保留为标记目的。
这是通过使用预写屏障来实现的(不要与后面讨论的写后屏障和与多线程编程相关的存储器障碍混淆)。它们的功能是,当G1并发标记处于活动状态时,无论何时写入字段,都将前一个裁判存储在所谓的日志缓冲区中,由并发标记线程处理。
1.364: [GC concurrent-mark-start]
1.645: [GC concurrent-mark-end, 0.2803470 secs]
阶段4.标记。这是一个stop-the-world的暂停,就像之前在CMS中看到的那样,完成了标记过程。对于G1,它会短暂停止应用程序线程以停止并发更新日志的流入并处理剩余的少量数据,并标记启动并发标记周期时仍然未标记的任何对象。此阶段还执行一些额外的清洁,例如参考处理(参见疏散暂停日志)或类卸载。
1.645: [GC remark 1.645: [Finalize Marking, 0.0009461 secs] 1.646: [GC ref-proc, 0.0000417 secs] 1.646: [Unloading, 0.0011301 secs], 0.0074056 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]
阶段5.清理。最后阶段为即将到来的疏散阶段做好准备,计算堆区域中的所有活动对象,并按预期的GC效率对这些区域进行排序。它还执行维持内部状态所需的所有内务活动,以便进行下一次并发标记迭代。
最后但并非最不重要的是,在此阶段中根本不回收不包含活动对象的区域。此阶段的某些部分是并发的,例如空区域回收和大部分活跃度计算,但它还需要一个短暂的stop-the-world暂停来完成图片,而应用程序线程不会干扰。这种stop-the-world停顿的日志类似于:
1.652: [GC cleanup 1213M->1213M(1885M), 0.0030492 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]
如果发现一些仅包含垃圾的堆区域,则暂停格式可能看起来有点不同,类似于:
1.872: [GC cleanup 1357M->173M(1996M), 0.0015664 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]
1.874: [GC concurrent-cleanup-start]
1.876: [GC concurrent-cleanup-end, 0.0014846 secs]
疏散暂停:Mixed
当并发清理可以释放老年代的整个区域时,这是一个令人愉快的情况,但情况可能并非总是如此。在Concurrent Marking成功完成之后,G1将安排一个混合收集,不仅会让垃圾远离年轻区域,还会将一堆老年代区域扔到收集集中。
混合疏散暂停并不总是紧跟在并发标记阶段的结束之后。有许多规则和启发式方法会影响这一点。例如,如果可以同时释放大部分老年代区域,则无需执行此操作。
因此,在并发标记结束和混合疏散暂停之间可能很容易出现一些完全年轻的疏散停顿。
还将基于许多规则选择要添加到垃圾收集集的老年代区域的确切数量以及它们的添加顺序。其中包括为应用程序指定的软实时性能目标,并发标记期间收集的活跃度和gc效率数据,以及许多可配置的JVM选项。混合收集的过程与我们之前已经针对完全年轻的gc已经回顾的过程大致相同,但这次我们还将介绍记忆集的主题。
记住的集合允许独立收集不同的堆区域。例如,当收集区域A,B和C时,我们必须知道区域D和E是否有对它们的引用以确定它们的活跃度。但是遍历整个堆图将花费相当长的时间并破坏增量收集的整个点,因此采用了优化。就像我们有用于在其他GC算法中独立收集年轻代区域的Card Table一样,我们在G1中有记忆集。
如下图所示,每个区域都有一个记忆集,列出了从外部指向该区域的引用。然后,这些引用将被视为额外的GC根。请注意,即使存在对它们的外部引用,老年代区域中在并发标记期间被确定为垃圾的对象也将被忽略:在这种情况下,所指对象也是垃圾。
接下来发生的事情与其他收集器的作用相同:多个并行GC线程可以找出哪些对象是活动的,哪些是垃圾:
最后,将活动对象移动到幸存者区域,必要时创建新对象。现在空的区域被释放,可用于再次存储对象。
为了维护记忆集,在应用程序运行期间,只要执行对字段的写入,就会发出写后屏障。如果得到的参考是跨区域的,即从一个区域指向另一个区域,则相应的条目将出现在目标区域的记忆集中。为了减少Write Barrier引入的开销,将卡放入Remembered Set的过程是异步的,并且具有许多优化功能。但基本上它归结为写屏障将脏卡信息放入本地缓冲区,以及专门的GC线程将其拾取并将信息传播到被引用区域的记忆集。
在混合模式下,与完全年轻模式相比,日志会发布一些新的有趣的东西:
[Update RS (ms): Min: 0.7, Avg: 0.8, Max: 0.9, Diff: 0.2, Sum: 6.1] [Processed Buffers: Min: 0, Avg: 2.2, Max: 5, Diff: 5, Sum: 18] [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.8] [Clear CT: 0.2 ms] [Redirty Cards: 0.1 ms]
- [Update RS (ms) - 由于同时处理了记忆集,我们必须确保在实际收集开始之前处理仍然缓冲的卡。如果此数字很高,则并发GC线程无法处理负载。例如,可能是由于大量的传入字段修改或CPU资源不足。
- [Processed Buffers - 每个工作线程处理了多少个本地缓冲区。
- [Scan RS (ms) - 扫描来自记忆集的参考文件需要多长时间。
- [Redirty Cards: 0.1 ms] - 清洁卡表中的卡片的时间。清理只是删除放在那里的“脏”状态,表示字段已更新,用于记忆集。
- [Redirty Cards:0.1 ms] - 将卡表中的适当位置标记为脏的时间。适当的位置由GC自身的堆突变定义,例如在排列引用时。
摘要
这应该让人们对G1的基本功能有充分的了解。当然,还有一些我们为了简洁而省略的实现细节,比如处理大量的对象,G1是HotSpot中技术最先进的生产就绪收集器。最重要的是,HotSpot工程师正在不断改进它,新的优化版或功能将与更新的Java版本一起出现。
正如我们所看到的,G1解决了CMS具有的各种问题,从暂停可预测性开始到堆碎片结束。鉴于应用程序不受CPU利用率的限制,但对单个操作的延迟非常敏感,G1很可能是HotSpot用户的最佳选择,尤其是在运行最新版本的Java时。但是,这些延迟改进并非没有代价:由于额外的写入障碍和更活跃的后台线程,G1的吞吐量开销更大。因此,如果应用程序受吞吐量限制或占用100%的CPU,并且不关心个别暂停持续时间,那么CMS甚至Parallel可能是更好的选择。
选择正确的GC算法和设置的唯一可行方法是通过试验和错误,但我们在下一章中给出了一般指导原则。
请注意,G1可能是Java 9的默认GC:http://openjdk.java.net/jeps/248
Shenandoah
我们已经概述了HotSpot中的所有生产就绪算法,您可以立即使用它们。还有一个正在制作中,即所谓的超低暂停时间垃圾收集器。它的目标是拥有大堆的大型多核机器,目标是管理100GB或更大的堆,暂停时间为10ms或更短。这与吞吐量相关:实施者的目标是,对于没有GC暂停的应用程序,性能损失不超过10%。
在新算法作为生产就绪版本发布之前,我们不打算详细介绍实现细节,但它也建立在前面章节中已经介绍过的许多想法之上,例如并发标记和增量收集。然而,它做了很多不同的事情。它不会将堆分成多代,而只是只有一个空格。没错,Shenandoah不是分代垃圾收集者。这使它可以摆脱卡表和记忆集。它还使用转发指针和Brooks样式读取屏障来允许并发复制活动对象,从而减少暂停的次数和持续时间。