JVM 垃圾收集器发展历史
并行GC出现在JDK1.8中使用的jmap -heap pid上
jmap-堆18378正在附加到进程ID 18378,请稍候.调试器已成功附加。检测到服务器编译器。JVM版本是25.261-B12,使用线程本地对象分配。4线程并行GC # # #在JVM垃圾收集器的发展史上,我们还没有发现并行GC,那么它代表什么?
使用Parallel GC有两种组合-XX: UseParallelGC参数可以将parallelscape和PSMarkSweep(串行旧)收集器组合起来进行垃圾收集。(可以在图上找到)使用-xx3360 userpanalleloldgc参数来启用并行清除和并行旧收集器的组合收集。(在图中可以找到)Parallel GC起源,Young GC / Parallel Scavenge的并行清除收集器(以下简称PS收集器)也是一个多线程收集器,它也使用复制算法。但是,它的对象分配规则和回收策略与ParNew collector不同。它被实现为一个收集器,目标是最大化吞吐量(即总运行时间中最小的GC时间),并且它允许交换一个长STW来最大化总吞吐量。
在Full GC / PSMarkSweep(Serial Old)'s并行扫气收集器架构中,有一个PS MarkSweep收集器用来收集陈年。但是因为PS MarkSweep和连环旧很接近,所以很多官方资料都是直接用连环旧来解释,而不是PS MarkSweep。
使用-XX: UseParallelGC参数打开PSScavenge和PSMarkSweep的组合,即只有年轻的GC是并行的,而完整的GC仍然是串行的,所以使用标记排序算法。
后来,Full GC / PSCompact(ParallelOld GC)的开发人员基于LISP2算法开发了一个并行版本的Full GC collector来收集整个GC堆,命名为PSCompact。使用-xx: useparallelism dgc参数打开PSScavenge和PSCompact的组合,并行化年轻gc和完整GC。
收集器新一代并行回收器,一种内存分配的复制算法。并行清除侧重于应用程序的吞吐量,而其他收集器则侧重于尽可能缩短STW(stop the word)的时间。
吞咽度量=t1/(t1 t2)t1运行用户代码的总时间t2运行垃圾收集的总时间。例如,虚拟机总共运行100分钟,其中垃圾收集需要1分钟,因此吞吐量为99%。并行清除收集器提供了两个参数来精确控制吞吐量,一个是-xx3360 maxgcpausemillis参数来控制最大垃圾收集暂停时间,另一个是-xx3360 gctimeratio参数来控制吞吐量。
-xx3360 maxgcpausemillis参数的值是大于0的毫秒数。收集器将尽力确保恢复时间不超过设定值。然而,越小越好,GC暂停时间越短,代价是吞吐量和新一代空间。如果设置值太小,会导致频繁的GC,这样虽然GC暂停时间下来了,但是吞吐量也下来了。比如收集500MB时,需要每10秒收集一次,每次恢复需要100ms;如果收集300MB,需要每5秒收集一次,每次需要70ms才能恢复。虽然每次恢复所需的时间更少,但工作频率增加,导致吞吐量下降。
-xx3360gtimeratio参数的值是一个大于0小于100的整数,即垃圾收集时间与总时间的比值。默认值为99,即允许的最大垃圾收集时间为1%(即1/(1 99))。
并行清除有一个重要的特性,它支持GC的自适应调整策略。它是通过使用-xx: useadaptivesizepolicy参数打开的。开启后,虚拟机根据当前系统运行情况收集监控信息,动态调整新世代比例、旧世代大小等详细参数,以提供最合适的暂停时间或最大吞吐量。打开该参数后,无需设置新生代大小、Eden与S0/S1之比等参数。
Parallel Scavenge
allel OldParallel Old GC 在 Parallel Scavenge 和 Parallel Old 收集器组合中,负责Full GC,是一个并行收集器,其在整理年轻代的时候,使用与Parallel Scavenge GC一样的常规“复制”算法,但是在整理老年代的时候,是使用的基于“标记-整理”算法优化的“Mark–Summary-Compaction”算法。
算法包含三个部分
Mark首先将老年代的内存,划分为大小固定的多个连续Region,当标记完存活对象之后,统计每个Region的存活对象数量。Mark阶段采用串行标记所有从GC Roots可直达的对象,然后并行标记所有存活的对象。
Summary某个Region的密度 = 存活对象的内存大小 / Region内存大小。因为每次整理会将存活的对象向Old区的左侧移动,而对象存活越久,理论上就越不容易被回收,所以经过多次整理之后,左侧Region中的对象更偏向于稳定、“长寿”,即是左侧Region的密度更大。Summary阶段,算法采用以空间换时间的优化方式,针对一个密度很大的Region,比如95%的空间是存活对象,只有断断续续5%的空间是未使用的,那么算法认为这个Region不值得被整理,即是选择浪费掉这5%的空间,以节省整理操作的时间开销。在Sumamry阶段,首先从左至右计算各个Region的密度,直到找到一个point,这个point左侧的Region都不值得整理,右侧的Region需要整理。point左侧的Region被称为dense prefix,这个区域内的对象都不会被移动。Summary阶段是一个串行执行的阶段。
CompactionCompaction阶段利用Summary阶段的统计数据,针对需要整理的部分,采用“整理”算法进行并行操作。
GC策略-XX:+ScavengeBeforeFullGCScavengeBeforeFullGC 是 Parallel GC
套装中(两种组合都生效)的一个参数,默认是开启的,作用是在一次Full GC之前,先触发一次Young GC来清理年轻代,以降低Full GC的STW耗时(Young GC会清理Young GC中非存活的对象,减少Full GC中,标记存活对象的工作量)。
举个例子,使用System.gc()触发Full GC,可以看到日志如下:
2020-03-01T13:38:30.496-0800:
添加 -XX:-ScavengeBeforeFullGC 参数之后,日志就变为只有一条Full GC的日志:
2020-03-01T14:26:05.562-0800:
当整个新生代剩余的空间无法存放某个对象时,Parallel GC中该对象会直接进入老年代;而如果整个新生代剩余的空间可以存放但只是Eden区空间不足,则会尝试一次Minor GC。举个例子:
public class TestApp { public static void main(String<> args) throws InterruptedException { allocM(10); allocM(10); allocM(10); allocM(20); Thread.sleep(1000); } private static byte<> allocM(int n) throws InterruptedException { byte<> ret = new byte<1024 * 1024 * n>; System.out.println(String.format("%s: Alloc %dMB", LocalDateTime.now().toString(), n)); Thread.sleep(500); return ret; }}JVM参数为: -Xms100m -Xmx100m -Xmn50m -XX:SurvivorRatio=8 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+UseParallelOldGC ,运行起来,打印日志如下:
2020-03-01T16:36:13.027: Alloc 10MB2020-03-01T16:36:13.548: Alloc 10MB2020-03-01T16:36:14.061: Alloc 10MB2020-03-01T16:36:14.577: Alloc 20MBHeap PSYoungGen total 46080K, used 38094K <0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000) eden space 40960K, 93% used <0x00000007bce00000,0x00000007bf333878,0x00000007bf600000) from space 5120K, 0% used <0x00000007bfb00000,0x00000007bfb00000,0x00000007c0000000) to space 5120K, 0% used <0x00000007bf600000,0x00000007bf600000,0x00000007bfb00000) ParOldGen total 51200K, used 20480K <0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000) object space 51200K, 40% used <0x00000007b9c00000,0x00000007bb000010,0x00000007bce00000) Metaspace used 4879K, capacity 5012K, committed 5248K, reserved 1056768K class space used 527K, capacity 564K, committed 640K, reserved 1048576K可以看到第4行,分配20M内存时,Eden区已经不足20M空余内存了,整个年轻代加起来都不够20M了,但是并没有触发Young GC,而是继续执行,知道程序结束前,打印堆的情况,我们可以看到20M内存是分配到了老年代中。
修改代码,将最后一个 allocM(20); 改成 allocM(5); ,重新执行,得到日志如下:
2020-03-01T16:39:56.375: Alloc 10MB2020-03-01T16:39:56.896: Alloc 10MB2020-03-01T16:39:57.408: Alloc 10MB{Heap before GC invocations=1 (full 0): PSYoungGen total 46080K, used 37274K <0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000) eden space 40960K, 91% used <0x00000007bce00000,0x00000007bf266a08,0x00000007bf600000) from space 5120K, 0% used <0x00000007bfb00000,0x00000007bfb00000,0x00000007c0000000) to space 5120K, 0% used <0x00000007bf600000,0x00000007bf600000,0x00000007bfb00000) ParOldGen total 51200K, used 0K <0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000) object space 51200K, 0% used <0x00000007b9c00000,0x00000007b9c00000,0x00000007bce00000) Metaspace used 4882K, capacity 5012K, committed 5248K, reserved 1056768K class space used 526K, capacity 564K, committed 640K, reserved 1048576K2020-03-01T16:39:57.910-0800:
悲观策略Parallel GC除了上述策略外,还有另外一个策略:在执行Young GC之后,如果晋升老年代的平均大小,比当前老年代的剩余空间要大的话,则会触发一次Full GC。
public class TestApp { public static void main(String<> args) throws InterruptedException { byte<><> use = new byte<7><>; use<0> = allocM(10); use<1> = allocM(10); use<2> = allocM(10); use<3> = allocM(10); use<4> = allocM(10); use<5> = allocM(10); use<6> = allocM(10); Thread.sleep(1000); } private static byte<> allocM(int n) throws InterruptedException { byte<> ret = new byte<1024 * 1024 * n>; System.out.println(String.format("%s: Alloc %dMB", LocalDateTime.now().toString(), n)); Thread.sleep(500); return ret; }}JVM参数为: -Xms100m -Xmx100m -Xmn50m -XX:SurvivorRatio=8 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+UseParallelOldGC ,运行起来,打印日志如下(省略掉部分):
2020-03-01T16:02:43.172: Alloc 10MB2020-03-01T16:02:43.693: Alloc 10MB2020-03-01T16:02:44.206: Alloc 10MB{Heap before GC invocations=1 (full 0): PSYoungGen total 46080K, used 37274K <*, *, *) eden space 40960K, 91% used <*,*,*) from space 5120K, 0% used <*,*,*) to space 5120K, 0% used <*,*,*) ParOldGen total 51200K, used 0K <*, *, *) object space 51200K, 0% used <*,*,*)2020-03-01T16:02:44.711-0800:
我们将JVM中的 -Xms100m -Xmx100m 换成 -Xms120m -Xmx120m ,重新执行日志如下:
2020-03-01T16:08:39.372: Alloc 10MB2020-03-01T16:08:39.895: Alloc 10MB2020-03-01T16:08:40.405: Alloc 10MB{Heap before GC invocations=1 (full 0): PSYoungGen total 46080K, used 37274K <*, *, *) eden space 40960K, 91% used <*,*,*) from space 5120K, 0% used <*,*,*) to space 5120K, 0% used <*,*,*) ParOldGen total 71680K, used 0K <*, *, *) object space 71680K, 0% used <*,*,*)2020-03-01T16:08:40.906-0800:
JVM默认老年代回收是 PSMarkSweep(Serial-Old) 还是Parallel Old?这个改进使得HotSpot VM在选择使用 ParallelGC (-XX:+UseParallelGC 或者是ergonomics自动选择)的时候,会默认开启 -XX:+UseParallelOldGC 。这个变更应该是在JDK7u4开始的JDK7u系列与JDK8系列开始生效。
http://hg.openjdk.java.net/jd...
--- a/src/share/vm/runtime/arguments.cpp Mon Jan 30 15:21:57 2012 +0100+++ b/src/share/vm/runtime/arguments.cpp Thu Feb 02 16:05:17 2012 -0800@@ -1400,10 +1400,11 @@ void Arguments::set_parallel_gc_flags() { assert(UseParallelGC || UseParallelOldGC, "Error");- // If parallel old was requested, automatically enable parallel scavenge.- if (UseParallelOldGC && !UseParallelGC && FLAG_IS_DEFAULT(UseParallelGC)) {- FLAG_SET_DEFAULT(UseParallelGC, true);+ // Enable ParallelOld unless it was explicitly disabled (cmd line or rc file).+ if (FLAG_IS_DEFAULT(UseParallelOldGC)) {+ FLAG_SET_DEFAULT(UseParallelOldGC, true); }+ FLAG_SET_DEFAULT(UseParallelGC, true); // If no heap maximum was requested explicitly, use some reasonable fraction // of the physical memory, up to a maximum of 1GB.在这个改变之前,即便选择了ParallelGC,默认情况下ParallelOldGC并不会随即开启,而是要自己通过 -XX:+UseParallelOldGC 去选定。
在GC日志里,如果看到Full GC里有"ParOldGen"就是选择了ParallelOldGC。
原文链接:https://segmentfault.com/a/1190000038390811
如果觉得本文对你有帮助,可以关注支持一下