美文网首页程序员
Java GC全集(四):GC 算法实现篇(一)串行GC

Java GC全集(四):GC 算法实现篇(一)串行GC

作者: HeapDump性能社区 | 来源:发表于2022-08-18 10:01 被阅读0次

    本文来自于HeapDump性能社区! !有性能问题,上HeapDump性能社区!
    正文:

    学习了GC算法的相关概念之后, 我们将介绍在JVM中这些算法的具体实现。首先要记住的是, 大多数JVM都需要使用两种不同的GC算法 —— 一种用来清理年轻代, 另一种用来清理老年代。

    我们可以选择JVM内置的各种算法。如果不通过参数明确指定垃圾收集算法, 则会使用宿主平台的默认实现。本章会详细介绍各种算法的实现原理。

    下面是关于Java 8中各种组合的垃圾收集器概要列表,对于之前的Java版本来说,可用组合会有一些不同:

    Young Tenured JVM options
    Incremental(增量GC) 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

    看起来有些复杂, 不用担心。主要使用的是上表中黑体字表示的这四种组合。其余的要么是被废弃(deprecated), 要么是不支持或者是不太适用于生产环境。所以,接下来我们只介绍下面这些组合及其工作原理:

    • 年轻代和老年代的串行GC(Serial GC)
    • 年轻代和老年代的并行GC(Parallel GC)
    • 年轻代的并行GC(Parallel New) + 老年代的CMS(Concurrent Mark and Sweep)
    • G1, 负责回收年轻代和老年代

    Serial GC(串行GC)

    Serial GC 对年轻代使用 mark-copy(标记-复制) 算法, 对老年代使用 mark-sweep-compact(标记-清除-整理)算法. 顾名思义, 两者都是单线程的垃圾收集器,不能进行并行处理。两者都会触发全线暂停(STW),停止所有的应用线程。

    因此这种GC算法不能充分利用多核CPU。不管有多少CPU内核, JVM 在垃圾收集时都只能使用单个核心。

    要启用此款收集器, 只需要指定一个JVM启动参数即可,同时对年轻代和老年代生效:

    java -XX:+UseSerialGC com.mypackages.MyExecutableClass
    

    该选项只适合几百MB堆内存的JVM,而且是单核CPU时比较有用。 对于服务器端来说, 因为一般是多个CPU内核, 并不推荐使用, 除非确实需要限制JVM所使用的资源。大多数服务器端应用部署在多核平台上, 选择 Serial GC 就表示人为的限制系统资源的使用。 导致的就是资源闲置, 多的CPU资源也不能用来降低延迟,也不能用来增加吞吐量。

    下面让我们看看Serial GC的垃圾收集日志, 并从中提取什么有用的信息。为了打开GC日志记录, 我们使用下面的JVM启动参数:

    -XX:+PrintGCDetails -XX:+PrintGCDateStamps 
    -XX:+PrintGCTimeStamps
    

    产生的GC日志输出类似这样(为了排版,已手工折行):

    ****-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]
    ****-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事件, 其中一次清理的是年轻代,另一次清理的是整个堆内存。让我们先来分析前一次GC,其在年轻代中产生。

    Minor GC(小型GC)

    以下代码片段展示了清理年轻代内存的GC事件:

    ****-05-26T14:45:37.987-02001 : 151.12622 : [ GC3 (Allocation Failure4 151.126:
    [DefNew5 : 629119K->69888K6 (629120K)7 , 0.0584157 secs] 1619346K->1273247K8
    (2027264K)9, 0.0585007 secs10] [Times: user=0.06 sys=0.00, real=0.06 secs]11
    
    1. ****-05-26T14:45:37.987-0200 – GC事件开始的时间. 其中-0200表示西二时区,而中国所在的东8区为 +0800
    1. 151.126 – GC事件开始时,相对于JVM启动时的间隔时间,单位是秒。
    1. GC – 用来区分 Minor GC 还是 Full GC 的标志。GC表明这是一次小型GC(Minor GC)
    1. Allocation Failure – 触发 GC 的原因。本次GC事件, 是由于年轻代中没有空间来存放新的数据结构引起的。
    1. DefNew – 垃圾收集器的名称。这个神秘的名字表示的是在年轻代中使用的: 单线程, 标记-复制(mark-copy), 全线暂停(STW) 垃圾收集器。
    1. 629119K->69888K – 在垃圾收集之前和之后年轻代的使用量。
    1. (629120K) – 年轻代总的空间大小。
    1. 1619346K->1273247K – 在垃圾收集之前和之后整个堆内存的使用情况。
    1. (2027264K) – 可用堆的总空间大小。
    1. 0.0585007 secs – GC事件持续的时间,以秒为单位。
    1. [Times: user=0.06 sys=0.00, real=0.06 secs] – GC事件的持续时间, 通过三个部分来衡量:
    • user – 在此次垃圾回收过程中, 所有 GC线程所消耗的CPU时间之和。

    • sys – GC过程中中操作系统调用和系统等待事件所消耗的时间。

    • real – 应用程序暂停的时间。因为串行垃圾收集器(Serial Garbage Collector)只使用单线程, 因此 real time 等于 user 和 system 时间的总和。

    可以从上面的日志片段了解到, 在GC事件中,JVM 的内存使用情况发生了怎样的变化。此次垃圾收集之前, 堆内存总的使用量为 1,619,346K。其中,年轻代使用了 629,119K。可以算出,老年代使用量为 990,227K

    更重要的信息蕴含在下一批数字中, 垃圾收集之后, 年轻代的使用量减少了 559,231K, 但堆内存的总体使用量只下降了 346,099K。 从中可以算出,有 213,132K 的对象从年轻代提升到了老年代。

    此次GC事件也可以用下面的示意图来说明, 显示的是GC开始之前, 以及刚刚结束之后, 这两个时间点内存使用情况的快照:

    21.png

    Full GC(完全GC)

    理解第一次 minor GC 事件后,让我们看看日志中的第二次GC事件:

    ****-05-26T14:45:59.690-02001 : 172.8292 : [GC (Allocation Failure 172.829:
    [DefNew: 629120K->629120K(629120K), 0.0000372 secs3] 172.829:[Tenured4:
    1203359K->755802K5 (1398144K)6, 0.1855567 secs7 ] 1832479K->755802K8
    (2027264K)9, [Metaspace: 6741K->6741K(1056768K)]10
    [Times: user=0.18 sys=0.00, real=0.18 secs]11

    >

    1. ****-05-26T14:45:59.690-0200 – GC事件开始的时间. 其中-0200表示西二时区,而中国所在的东8区为 +0800
    2. 172.829 – GC事件开始时,相对于JVM启动时的间隔时间,单位是秒。
    3. [DefNew: 629120K->629120K(629120K), 0.0000372 secs – 与上面的示例类似, 因为内存分配失败,在年轻代中发生了一次 minor GC。此次GC同样使用的是 DefNew 收集器, 让年轻代的使用量从 629120K 降为 0。注意,JVM对此次GC的报告有些问题,误将年轻代认为是完全填满的。此次垃圾收集消耗了 0.0000372秒。
    1. Tenured – 用于清理老年代空间的垃圾收集器名称。Tenured 表明使用的是单线程的全线暂停垃圾收集器, 收集算法为 标记-清除-整理(mark-sweep-compact )。
    1. 1203359K->755802K – 在垃圾收集之前和之后老年代的使用量。
    2. (1398144K) – 老年代的总空间大小。
    3. 0.1855567 secs – 清理老年代所花的时间。
    4. 1832479K->755802K – 在垃圾收集之前和之后,整个堆内存的使用情况。
    5. (2027264K) – 可用堆的总空间大小。
    6. [Metaspace: 6741K->6741K(1056768K)] – 关于 Metaspace 空间, 同样的信息。可以看出, 此次GC过程中 Metaspace 中没有收集到任何垃圾。
    7. [Times: user=0.18 sys=0.00, real=0.18 secs] – GC事件的持续时间, 通过三个部分来衡量:
    • user – 在此次垃圾回收过程中, 所有 GC线程所消耗的CPU时间之和。
    • sys – GC过程中中操作系统调用和系统等待事件所消耗的时间。
    • real – 应用程序暂停的时间。因为串行垃圾收集器(Serial Garbage Collector)只使用单线程, 因此 real time 等于 user 和 system 时间的总和。

    和 Minor GC 相比,最明显的区别是 —— 在此次GC事件中, 除了年轻代, 还清理了老年代和Metaspace. 在GC事件开始之前, 以及刚刚结束之后的内存布局,可以用下面的示意图来说明:


    22.png

    在本手册中,我们将介绍JVM中垃圾收集的实现原理,以及如何高效地利用GC。
    第一篇:什么是垃圾回收?

    第二篇:Java 中的垃圾收集原理解析

    第三篇:GC算法基础篇

    第四篇:GC 算法实现篇——串行GC

    第五篇:GC 算法实现篇——并行GC

    第六篇:GC 算法实现篇——并发标记-清除

    第七篇:GC 算法实现篇——垃圾优先算法

    第八篇:GC 调优基础篇

    第九篇:GC 调优工具篇

    第十篇:GC调优实战篇—高分配速率(High Allocation Rate)

    第十一篇:GC 调优的实战篇—过早提升(Premature Promotion)

    第十二篇:GC 调优的实战篇—Weak, Soft 及 Phantom 引用

    相关文章

      网友评论

        本文标题:Java GC全集(四):GC 算法实现篇(一)串行GC

        本文链接:https://www.haomeiwen.com/subject/lchegrtx.html