0%

Java - Serial GC 与 ParNew GC

Serial GC,串行垃圾收集器,在Java语言的发展历程中,可以算是一个历史很悠久的垃圾回收器了。

虽然说随着硬件的迅猛发展,不断出现了更适应高性能服务的垃圾回收器,JDK11推出的ZGC,JDK12推出的Shenandoah GC,更使开发人员比以往任何时候都接近无暂停(STW,stop the word)时间。在这个时候使用串行垃圾收集器显得有点格格不入,不过这并不影响我们通过它去了解收集器的一些基本知识。

Serial

通过使用-XX:+UseSerialGC参数,来启用SerialGC组合,其中Young GC采用Serial GC,Full GC采用Serial Old GC。其中Serial GC采用的是复制算法,Serial Old GC采用的是标记-整理算法,二者在工作时都是串行的,并且都需要STW。

运行示意图如下:

1

可以看到当所有用户线程到达safe point之后,只有一个GC线程在执行,GC完成之后,用户线程才会恢复。在当前硬件高速发展的环境下,这种单线程串行工作的GC无法利用高性能服务器多CPU的优点,会导致STW的时间比多线程的方式长很多。目前这种串行GC的组合只有在一个CPU的环境中,并且运行几百兆堆大小的JVM下才有意义。

SerialGC日志分析

GC日志通常是分析GC问题最有效的方式,添加如下JVM参数,可以将GC的详细情况打印出来。

  • -verbose:gc:开启GC日志打印;
  • -XX:+PrintGCDetails:打印GC日志时输出更多详细信息;
  • -XX:+PrintGCDateStamps:打印GC时的时间;
  • -XX:+PrintHeapAtGC:在GC前后打印堆的内存占用信息。
  • -XX:PretenureSizeThreshold=15m:将大于15m的对象直接分配到老年代

Young GC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{Heap before GC invocations=0 (full 0):
def new generation total 46080K, used 37683K [0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000)
eden space 40960K, 92% used [0x00000007b9c00000, 0x00000007bc0ccef0, 0x00000007bc400000)
from space 5120K, 0% used [0x00000007bc400000, 0x00000007bc400000, 0x00000007bc900000)
to space 5120K, 0% used [0x00000007bc900000, 0x00000007bc900000, 0x00000007bce00000)
tenured generation total 51200K, used 20480K [0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000)
the space 51200K, 40% used [0x00000007bce00000, 0x00000007be200010, 0x00000007be200200, 0x00000007c0000000)
Metaspace used 4785K, capacity 4948K, committed 5248K, reserved 1056768K
class space used 520K, capacity 564K, committed 640K, reserved 1048576K

2020-02-29T11:06:49.419-0800: [GC (Allocation Failure) [DefNew: 37683K->3147K(46080K), 0.0032378 secs] 58163K->23627K(97280K), 0.0032571 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]

Heap after GC invocations=1 (full 0):
def new generation total 46080K, used 3147K [0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000)
eden space 40960K, 0% used [0x00000007b9c00000, 0x00000007b9c00000, 0x00000007bc400000)
from space 5120K, 61% used [0x00000007bc900000, 0x00000007bcc12ed0, 0x00000007bce00000)
to space 5120K, 0% used [0x00000007bc400000, 0x00000007bc400000, 0x00000007bc900000)
tenured generation total 51200K, used 20480K [0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000)
the space 51200K, 40% used [0x00000007bce00000, 0x00000007be200010, 0x00000007be200200, 0x00000007c0000000)
Metaspace used 4785K, capacity 4948K, committed 5248K, reserved 1056768K
class space used 520K, capacity 564K, committed 640K, reserved 1048576K
}

上面的日志分为三部分:

  • GC前堆的内存占用情况;
  • GC日志;
  • GC后堆的内存占用情况。

GC日志为:2020-02-29T11:06:49.419-0800: [GC (Allocation Failure) [DefNew: 37683K->3147K(46080K), 0.0032378 secs] 58163K->23627K(97280K), 0.0032571 secs][Times: user=0.00 sys=0.01, real=0.01 secs],其中:

  • 2020-02-29T11:06:49.419-0800:发生GC时候的时间戳;

  • GC:区分收集区域,“GC”表示是一次Young GC,还有“Full GC”;

    周志明的《深入理解Java虚拟机》一文中在解释GC日志时提到:GC日志开头的“GC”和“Full GC”说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有“Full”,说明这次GC是发生了Stop-The-World的,这句话也被很多博客引用,其观点与实际情况并不一致,比如此处SerialGC明明是STW的,但是以GC开头就表示没有STW?

    这句话令我百思不得其解,幸好看到了大神R大的观点,确定了是书中有误。

  • Allocation Failure:发生GC原因,此处表示是在Eden中分配内存失败,触发了一次Young GC;

  • DefNew: 37683K->3147K(46080K), 0.0032378 secs:

    • DefNew:使用的垃圾收集器名称,这里使用单线程标记-复制,且会STW的垃圾收集器;
    • 37683K:GC前年轻代的内存占用大小;
    • 3147K:GC后年轻代的内存占用大小;
    • 46080K:年轻代的总大小;
    • 0.0032378:年轻代GC的耗时;
  • 58163K->23627K(97280K), 0.0032571 secs:

    • 58163K:整个堆回收前的内存占用大小;
    • 23627K:整个堆回收后的内存占用大小;
    • 97280K:整个堆的总大小;
    • 0.0032571:整个GC耗时;
  • Times: user=0.00 sys=0.01, real=0.01 secs:

    • user:用户线程占用的CPU总时间;
    • sys:内核调用占用的CPU时间
    • real:应用整体暂停时长。

Full GC

Full GC日志为:2020-02-29T11:06:49.641-0800: [Full GC (System.gc()) [Tenured: 40960K->23595K(51200K), 0.0066077 secs] 55166K->23595K(97280K), [Metaspace: 4785K->4785K(1056768K)], 0.0066468 secs] [Times: user=0.01 sys=0.00, real=0.00 secs],其中:

  • 2020-02-29T11:06:49.641-0800:发生GC时候的时间戳;
  • Full GC:区分收集区域;
  • System.gc():发生GC原因,此处表示是调用了System.gc()函数触发的Full GC;
  • Tenured: 40960K->23595K(51200K), 0.0066077 secs:
    • Tenured:清理老年代的垃圾收集器名称,这里使用单线程标记-整理,且会STW的垃圾收集器;
    • 40960K:GC前老年代的内存占用大小;
    • 23595K:GC后老年代的内存占用大小;
    • 51200K:老年代的总大小;
    • 0.0066077:老年代GC的耗时;
  • 55166K->23595K(97280K):
    • 55166K:整个堆回收前的内存占用大小;
    • 23595K:整个堆回收后的内存占用大小;
    • 97280K:整个堆的总大小;
  • Metaspace: 4785K->4785K(1056768K):
    • 4785K:整个Metaspace回收前的内存占用大小;
    • 4785K:整个Metaspace回收后的内存占用大小;
    • 1056768K:这个值涉及到的Metaspace内存大小比较复杂,可以参考笨神的这篇文章
  • 0.0066468 secs:此次GC总耗时;
  • Times: user=0.01 sys=0.00, real=0.00 secs:
    • user:用户线程占用的CPU总时间;
    • sys:内核调用占用的CPU时间
    • real:应用整体暂停时长。

ParNew

ParNew是一个工作在新生代的垃圾回收期,是Serial收集器的多线程版本,使用-XX:+UseParNewGC参数来启用ParNew和Serial Old收集器组合进行垃圾收集。

他只是简单的将Serial回收器多线程化,他的控制参数,回收策略,算法等和Serial回收器一样。在进行垃圾回收的时候也需要STW。可以使用-XX:ParallelGCThreads=N参数设置工作时的线程数,建议设置成与主机CPU数一致。

运行示意图如下:

2



-=全文完=-