关于jvm:JavaParallel-GC介绍

36次阅读

共计 12619 个字符,预计需要花费 32 分钟才能阅读完成。

JVM 垃圾收集器倒退历史

JDK1.8 中应用 jmap -heap pid 下面会呈现Parallel GC

jmap -heap 18378
Attaching to process ID 18378, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12

using thread-local object allocation.
Parallel GC with 4 thread(s) ###

JVM 垃圾收集器的倒退历史中, 咱们并没有找到Parallel GC, 那么它到底代表什么?

Parallel GC 有两种组合

  • 应用 -XX:+UseParallelGC 参数来启用 Parallel ScavengePSMarkSweep(Serial Old)收集器组合进行垃圾收集。(图上能够找到)
  • 应用 -XX:+UserParallelOldGC 参数来启用 Parallel scavengeParallel Old收集器组合收集。(图上能够找到)

Parallel GC 起源

Young GC / Parallel Scavenge

Parallel Scavenge 收集器(下称 PS 收集器)也是一个多线程收集器,也是应用复制算法,但它的对象调配规定与回收策略都与 ParNew 收集器有所不同,它是以吞吐量最大化(即 GC 工夫占总运行工夫最小)为指标的收集器实现,它容许较长时间的 STW 换取总吞吐量最大化。

Full GC / PSMarkSweep(Serial Old)

在 Parallel Scavenge 收集器架构中自身有 PS MarkSweep 收集器来进行老年代收集,但因为 PS MarkSweep 与 Serial Old 实现十分靠近,因而官网的许多材料都间接以 Serial Old 代替 PS MarkSweep 进行解说。

应用 -XX:+UseParallelGC 参数便是开启 PSScavengePSMarkSweep的组合,即是只有 Young GC 是并行的,Full GC依然是串行, 应用 标记 - 整顿 算法。

Full GC / PSCompact(ParallelOld GC)

起初开发者开发了基于 LISP2 算法的并行版的 Full GC 收集器来收集整个 GC 堆,名为 PSCompact。应用-XX:+UseParallelOldGC 参数来便是开启 PSScavengePSCompact的组合,将 Young GCFull GC都并行化了。

收集器

Parallel Scavenge

新生代并行回收器,内存散布应用的复制算法。Parallel Scavenge次要关注的是利用的吞吐量,而其余收集器关注的次要是尽可能的缩短 STW(stop the word)的工夫。

吞度量 =t1/(t1+t2)
t1 运行用户代码的总工夫
t2 运行垃圾收集的总工夫
比方,虚拟机总共运行了 100 分钟,其中垃圾收集花掉 1 分钟,那吞吐量就是 99%。

Parallel Scavenge收集器提供了两个参数来用于准确管制吞吐量,一是管制最大垃圾收集进展工夫的 -XX:MaxGCPauseMillis 参数,二是管制吞吐量大小的 -XX:GCTimeRatio 参数

  • -XX:MaxGCPauseMillis

参数的值是一个大于 0 的毫秒数,收集器将尽可能的保障回收消耗的工夫不超过设定的值,然而,并不是越小越好,GC 进展工夫缩短是以就义吞吐量和新生代空间来换取的,如果设置的值太小,将会导致频繁 GC,这样尽管 GC 进展工夫下来了,然而吞吐量也下来了。比方收集 500MB 时候,须要每 10 秒收集一次,每次回收耗时 100ms;如果收集 300MB 的时候,须要每 5 秒收集一次,每次回收耗时 70ms,尽管每次回收耗时更少,然而工作频次进步,导致吞吐量反而升高了。

  • -XX:GCTimeRatio

参数的值是一个大于 0 且小于 100 的整数,也就是垃圾收集工夫占总工夫的比率,默认值是 99,就是容许最大 1%(即 1 /(1+99))的垃圾收集工夫。

Parallel Scavenge 有个重要的个性,是反对 GC 自适应的调节策略,应用 -XX:UseAdaptiveSizePolicy 参数开启,开启之后,虚构机会依据以后零碎运行状况收集监控信息,动静调整新生代的比例、老年大大小等细节参数,以提供最合适的进展工夫或最大的吞吐量。开启这个参数之后,就不须要再设置新生代大小,Eden 与 S0/S1 的比例等等参数。

Parallel Old

Parallel Old GCParallel ScavengeParallel 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 阶段是一个串行执行的阶段。

  • Compaction

Compaction 阶段利用 Summary 阶段的统计数据,针对须要整顿的局部,采纳“整顿”算法进行并行操作。

GC 策略

  • -XX:+ScavengeBeforeFullGC

ScavengeBeforeFullGCParallel 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: [GC (System.gc()) [PSYoungGen: 37274K->1392K(46080K)] 78234K->42360K(97280K), 0.0033397 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] 
2020-03-01T13:38:30.500-0800: [Full GC (System.gc()) [PSYoungGen: 1392K->0K(46080K)] [ParOldGen: 40968K->1225K(51200K)] 42360K->1225K(97280K), [Metaspace: 4876K->4876K(1056768K)], 0.0113851 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]

第一次 GC 为一次 Young GC,能够看到是由 System.gc()触发的,而后紧跟着是一次 Full GC。

增加 -XX:-ScavengeBeforeFullGC 参数之后,日志就变为只有一条 Full GC 的日志:

2020-03-01T14:26:05.562-0800: [Full GC (System.gc()) [PSYoungGen: 37274K->0K(46080K)] [ParOldGen: 40960K->1225K(51200K)] 78234K->1225K(97280K), [Metaspace: 4882K->4882K(1056768K)], 0.0127785 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]

内存调配策略

对于惯例收集器来说,当 Eden 区无奈分配内存时,便会触发一次 Young GC,然而对于 Parallel GC 有点变动:

  • 当整个新生代残余的空间无奈寄存某个对象时,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 10MB
2020-03-01T16:36:13.548: Alloc 10MB
2020-03-01T16:36:14.061: Alloc 10MB
2020-03-01T16:36:14.577: Alloc 20MB
Heap
 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 10MB
2020-03-01T16:39:56.896: Alloc 10MB
2020-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 1048576K
2020-03-01T16:39:57.910-0800: [GC (Allocation Failure) [PSYoungGen: 37274K->1328K(46080K)] 37274K->1336K(97280K), 0.0033380 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 1328K [0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 40960K, 0% used [0x00000007bce00000,0x00000007bce00000,0x00000007bf600000)
  from space 5120K, 25% used [0x00000007bf600000,0x00000007bf74c010,0x00000007bfb00000)
  to   space 5120K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007c0000000)
 ParOldGen       total 51200K, used 8K [0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000)
  object space 51200K, 0% used [0x00000007b9c00000,0x00000007b9c02000,0x00000007bce00000)
 Metaspace       used 4882K, capacity 5012K, committed 5248K, reserved 1056768K
  class space    used 526K, capacity 564K, committed 640K, reserved 1048576K
}

2020-03-01T16:39:57.916: Alloc 5MB

在执行第 4 行,调配 5M 内存时,Eden 区有余,然而整个年老代空余内存是大于 5M 的,于是触发了一次 Young GC。

乐观策略

绝大多数收集器,都有这么一个策略:在执行 Young GC 之前,如果预计之前降职老年代的均匀大小,比以后老年代的残余空间要大的话,则会放弃 Young GC,转而触发 Full GC。

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 10MB
2020-03-01T16:02:43.693: Alloc 10MB
2020-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: [GC (Allocation Failure) [PSYoungGen: 37274K->1392K(46080K)] 37274K->32120K(97280K), 0.0163176 secs] [Times: user=0.09 sys=0.03, real=0.01 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 51200K, used 30728K [*, *, *)
  object space 51200K, 60% used [*,*,*)
}

{Heap before GC invocations=2 (full 1):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 51200K, used 30728K [*, *, *)
  object space 51200K, 60% used [*,*,*)
2020-03-01T16:02:44.728-0800: [Full GC (Ergonomics) [PSYoungGen: 1392K->0K(46080K)] [ParOldGen: 30728K->31945K(51200K)] 32120K->31945K(97280K), [Metaspace: 4881K->4881K(1056768K)], 0.0096352 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
Heap after GC invocations=2 (full 1):
 PSYoungGen      total 46080K, used 0K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 0% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 51200K, used 31945K [*, *, *)
  object space 51200K, 62% used [*,*,*)
}

2020-03-01T16:02:44.739: Alloc 10MB

执行完第一次 Young GC 之后,因为年老代的 S 区容量有余,所以 Eden 区中的 30M 内存会提前降职到老年代。GC 之后,老年代空间被占用了 60%,还剩下 40%(20M),而均匀降职内存大小为 30M,所以触发乐观策略,导致了一次Full GC

咱们将 JVM 中的 -Xms100m -Xmx100m 换成-Xms120m -Xmx120m,从新执行日志如下:

2020-03-01T16:08:39.372: Alloc 10MB
2020-03-01T16:08:39.895: Alloc 10MB
2020-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: [GC (Allocation Failure) [PSYoungGen: 37274K->1360K(46080K)] 37274K->32088K(117760K), 0.0152322 secs] [Times: user=0.07 sys=0.03, real=0.02 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 1360K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 26% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 30728K [*, *, *)
  object space 71680K, 42% used [*,*,*)
}

2020-03-01T16:08:40.923: Alloc 10MB
2020-03-01T16:08:41.429: Alloc 10MB
2020-03-01T16:08:41.934: Alloc 10MB

{Heap before GC invocations=2 (full 0):
 PSYoungGen      total 46080K, used 32854K [*, *, *)
  eden space 40960K, 76% used [*,*,*)
  from space 5120K, 26% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 30728K [*, *, *)
  object space 71680K, 42% used [*,*,*)
2020-03-01T16:08:42.438-0800: [GC (Allocation Failure) [PSYoungGen: 32854K->1392K(46080K)] 63582K->62840K(117760K), 0.0151558 secs] [Times: user=0.07 sys=0.03, real=0.02 secs] 
Heap after GC invocations=2 (full 0):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 61448K [*, *, *)
  object space 71680K, 85% used [*,*,*)
}

{Heap before GC invocations=3 (full 1):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 61448K [*, *, *)
  object space 71680K, 85% used [*,*,*)
 Metaspace       used 4883K, capacity 5012K, committed 5248K, reserved 1056768K
  class space    used 526K, capacity 564K, committed 640K, reserved 1048576K
2020-03-01T16:08:42.454-0800: [Full GC (Ergonomics) [PSYoungGen: 1392K->0K(46080K)] [ParOldGen: 61448K->62634K(71680K)] 62840K->62634K(117760K), [Metaspace: 4883K->4883K(1056768K)], 0.0139615 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 
Heap after GC invocations=3 (full 1):
 PSYoungGen      total 46080K, used 0K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 0% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 62634K [*, *, *)
  object space 71680K, 87% used [*,*,*)
}

2020-03-01T16:08:42.469: Alloc 10MB

能够看到,第一次 Young GC 之后,因为老年代残余空间足够大,并没有触发 Full GC,而随着内存持续调配,第二次 Young GC 之后,还是触发了乐观策略。

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。
[Full GC [PSYoungGen: 480K->0K(3584K)] [ParOldGen: 4660K->4909K(12288K)] 5141K->4909K(15872K) [PSPermGen: 11202K->11198K(22528K)], 0.0515530 secs] [Times: user=0.08 sys=0.00, real=0.05 secs]

正文完
 0