JVM优化系列-JVM G1 垃圾收集器

导语
  G1回收器是在JDK1.7中正式使用的一种全新的垃圾回收器,它的目标是为了取代CMS回收器。G1回收器拥有独特的垃圾回收策略,和之前的任意的一种垃圾回收器都有所不同,但是从分代策略上来说依然是属于分代垃圾回收器,也分为年轻代和老年代,任然是有eden区和survivor区。但从堆结构上看,它并不要求整个的eden区、年轻代或者老年代是连续的空间。它所使用的是一个分区算法,这个后面的分享中会介绍到。

文章目录

    • G1特点
    • G1垃圾收集器参数
    • G1 内存划分和主要收集过程
    • G1 的新生代GC
      • 日志分析
    • G1 的并发标记周期
      • 标记周期日志分析
    • 混合回收
      • G1 的 Full GC操作
  • 总结

G1特点

  作为CMS的替代算法,G1使用了全新的分区算法特点如下

并行性 :G1在回收期间,可以由多个GC线程同时工作,有效的利用多核的计算能力
并发性 :G1拥有与应用程序交替执行的能力,部分工作可以与应用程序同时执行,所以,不会在一整个回收期间完全阻塞应用程序。
分代GC:G1 是一个分代收集器,与之前的收集器不同的是它兼顾了年轻代和老年代,相比较于其他的垃圾回收器,其他的收集器只能工作于年轻代或者老年代其中的一个,所以在这个方面和之前的收集器有所不同。
空间整理:G1 在回收过程中会进行适当的对象移动,与CMS不同,只是简单的标记清理对象,在若干次GC之后,CMS必须执行一次碎片整理,G1则是每次回收都会有效的复制对象,尽量减少空间碎片。
可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,可以缩小回收范围,对于全局停顿有很好的掌控性。

G1垃圾收集器参数

选项/默认值 说明
-XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器
-XX:MaxGCPauseMillis=n 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标.
-XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45.
-XX:NewRatio=n 新生代与老生代(new/old generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=n eden/survivor 空间大小的比例(Ratio). 默认值为 8.
-XX:MaxTenuringThreshold=n 提升年老代的最大临界值(tenuring threshold). 默认值为 15.
-XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.
-XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同.
-XX:G1ReservePercent=n 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
-XX:G1HeapRegionSize=n 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.

G1 内存划分和主要收集过程

  G1收集器将堆进行分区,将内存分为不同的区域,每次回收的时候,只回收其中的几个区域,这样可以有效的来控制垃圾回收产生的一次停顿时间。

  G1 的收集过程有以下的4个阶段

  • 新生代GC
  • 并发标记周期
  • 混合收集
  • Full GC

G1 的新生代GC

  新生代的GC主要工作是回收eden区和survivor区。一旦eden区被占用满,新生代GC就会启动。如下图所示
JVM优化系列-JVM G1 垃圾收集器_第1张图片
  其中E表示eden区,S表示survivor区,O表示老年代。可以看出来在新生代GC只处理eden区和survivor区,回收之后所有的Eden区都应该是被清空,survivor去被回收部分数据,但至少会存在一个survivor区。与其他的收集器来讲这点的变化不会太大。另外的变化就是老年代的区域会增加,这是因为部分survivor区或者是eden区的对象可能会升级到老年代。

JVM优化系列-JVM G1 垃圾收集器_第2张图片

日志分析

-Xmx1g -Xms1g -Xmn900m -XX:+UseG1GC -Xloggc:gc.log -XX:+PrintGCDetails

观察日志情况会看到与之前的垃圾回收日志信息有所不同


10.519: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0359476 secs]
   [Parallel Time: 34.8 ms, GC Workers: 13]
      [GC Worker Start (ms): Min: 10519.2, Avg: 10519.3, Max: 10519.4, Diff: 0.2]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 2.8]
      [Update RS (ms): Min: 1.2, Avg: 1.3, Max: 1.5, Diff: 0.4, Sum: 17.0]
         [Processed Buffers: Min: 2, Avg: 2.1, Max: 3, Diff: 1, Sum: 27]
      [Scan RS (ms): Min: 3.6, Avg: 3.7, Max: 3.8, Diff: 0.2, Sum: 48.1]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 29.3, Avg: 29.3, Max: 29.4, Diff: 0.1, Sum: 381.2]
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Termination Attempts: Min: 1, Avg: 1.9, Max: 3, Diff: 2, Sum: 25]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]
      [GC Worker Total (ms): Min: 34.5, Avg: 34.6, Max: 34.7, Diff: 0.2, Sum: 449.4]
      [GC Worker End (ms): Min: 10553.8, Avg: 10553.8, Max: 10553.9, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.2 ms]
   [Other: 0.9 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.2 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.3 ms]
   [Eden: 72.0M(69.0M)->0.0B(107.0M) Survivors: 8192.0K->10.0M Heap: 458.0M(800.0M)->461.5M(800.0M)]
 [Times: user=0.17 sys=0.19, real=0.04 secs] 
10.555: [GC concurrent-root-region-scan-start]
10.556: [GC concurrent-root-region-scan-end, 0.0010221 secs]
10.556: [GC concurrent-mark-start]
10.655: [GC concurrent-mark-end, 0.0992927 secs]
10.656: [GC remark 10.656: [Finalize Marking, 0.0012177 secs] 10.657: [GC ref-proc, 0.0000598 secs] 10.657: [Unloading, 0.0004519 secs], 0.0036237 secs]
 [Times: user=0.02 sys=0.00, real=0.00 secs] 
10.659: [GC cleanup 466M->462M(800M), 0.0013445 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
10.661: [GC concurrent-cleanup-start]
10.661: [GC concurrent-cleanup-end, 0.0000085 secs]
 
 

  和其他收集器的日志相比G1的日志内容比较丰富,从日志中可以看到,Eden区的使用情况,以及Survivors的使用情况。

[Eden: 900.0M(900.0M)->0.0B(796.0M) Survivors: 0.0B->104.0M Heap: 915.5M(1024.0M)->452.5M(1024.0M)]

G1 的并发标记周期

  G1并发阶段与CMS类似,都是为了降低一次的停顿时间,可以将与应用程序一起执行的部分提取然后执行。主要可以分为如下的步骤

  • 初始标记 :标记从根节点直接可以达到的对象,这个阶段可以从日志中看到,会伴随一次新生代的GC,会产生全局停顿,应用程序在这个阶段必须停顿。
  • 根区域扫描 :由于初始标记必然会伴随一次新生代GC,所以在初始化标记之后,eden被清空,并且存活对象被移入survivor区。这个阶段中,将扫描survivor区直接可达的老年区域,并且标记这些直接可达的对象。而这个过程可以和应用程序并发执行。但是根区域扫描不能和新生代GC同时执行,所以如果在这个时候需要进行新生代GC,GC就需要等待根区域扫描结束之后才能进行,如果发生这种情况,新生代的GC时间就会延长。
  • 并发标记 :和CMS类似,并发标记将会扫描整个堆的存活对象,并做好标记。这个过程是并发的,并且这个过程可以被一次新生代GC打断。
  • 重新标记 与CMS一样,重新标记会产生应用程序的停顿,由于在并发标记的过程中应用程序依然在运行,所以标记结果可能需要从新修正,所以在对上次标记的结果进行补充,在G1中,这个整个的过程是通过SATB(Snapshot-At-The-Beginning)算法来完成的。也就是G1会在标记之前给存活的对象创建一个快照,这个快照有助于加速重新标记的速度。
  • 独占清理 在这个阶段会引起程序的停顿,计算各个区域的存活对象和GC的回收比例进行排序,识别可以提供混合回收的区域,这个阶段还会更新记忆集(Remebered Set),给出需要利用混合回收的区域并标记。
  • 并发清理阶段 识别清理完全空闲的区域,由于是并发清理不会引起停顿。

并发标记周期前
JVM优化系列-JVM G1 垃圾收集器_第3张图片

  如图所示,在并发标记周期前堆内存使用情况,由于并发标记周期包含一次新生代的GC,所以新生代会进行整理,但由于并发标记执行时,程序依然处于运行期,所以并发标记之后又有新的eden空间被使用。
并发标记周期后
JVM优化系列-JVM G1 垃圾收集器_第4张图片
  并发标记周期执行前后最大的不同是在新生代GC之后,系统会增加一些标记为G的区域,这些区域被标记,因为它们内部的垃圾比例较高,所以在之后的混合GC中进行收集(在并发标记周期中并没有正式回收这些区域)。这些将要被回收的区域会被G1记录在Collection Sets的集合中,在之前的日志中可以看到。
JVM优化系列-JVM G1 垃圾收集器_第5张图片

标记周期日志分析

1、初始标记 伴随一次新生代的GC

10.519: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0359476 secs]

[Eden: 72.0M(69.0M)->0.0B(107.0M) Survivors: 8192.0K->10.0M Heap: 458.0M(800.0M)->461.5M(800.0M)]

  可以看到在初始化,eden区域被清空。

2、并发扫描根区域

10.555: [GC concurrent-root-region-scan-start]
10.556: [GC concurrent-root-region-scan-end, 0.0010221 secs]

3、并发标记


10.556: [GC concurrent-mark-start]
10.655: [GC concurrent-mark-end, 0.0992927 secs]

4、重新标记
  会引起全局停顿


10.656: [GC remark 10.656: [Finalize Marking, 0.0012177 secs] 10.657: [GC ref-proc, 0.0000598 secs] 10.657: [Unloading, 0.0004519 secs], 0.0036237 secs]

5、独占清理
  独占清理会重新计算各个区域的存活对象,并以此可以得到每个区域进行回收的比例。


10.659: [GC cleanup 466M->462M(800M), 0.0013445 secs]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
 

6、并发清理
  根据独占清理阶段计算得出的每个区域的存活对象数量,直接回收已经不包含存活对象的区域

10.661: [GC concurrent-cleanup-start]
10.661: [GC concurrent-cleanup-end, 0.0000085 secs]

测试代码

public class StopWorldTest {
    public static class MyThread extends Thread{
        HashMap map = new HashMap();

        @Override
        public void run() {
            try{
                while (true){
                    if (map.size()*512/1024/1024>=500){
                        map.clear();
                        System.out.println("clean map");
                    }
                    byte[] b1 ;
                    for (int i =0;i<100;i++){
                        b1 = new byte[512];
                        map.put(System.nanoTime(),b1);
                    }
                    Thread.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class PrintThread extends Thread{
        public static final long starttime = System.currentTimeMillis();

        @Override
        public void run() {
            try {
                while (true){
                    long t = System.currentTimeMillis()-starttime;
                    System.out.println(t/1000+"."+t%1000);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public static void main(String[] args) {
        MyThread t = new MyThread();
        PrintThread p = new PrintThread();
        t.setName("MyThread");
        p.setName("PrintThread");
        t.start();
        p.start();
    }
}

虚拟机参数

-Xmx800m -Xms800m -XX:+UseG1GC -Xloggc:gc.log -XX:+PrintGCDetails

混合回收

  在上面的并发标记周期中,虽然是有一部分对象被回收,但是从总体上来讲,回收的比率是比较低的。并发标记带来的最大的好处就是G1可以明确知道哪些区域所包含的垃圾较多,在混合回收的时候就可以专门对这些区域进行回收。当然G1会优先回收垃圾比例较高的区域,因为这些区域的回收性价比比较高。这就G1( Garbage First Garbage Collector)
  这个整合的阶段被称为是混合回收,因为在这个阶段会执行正常的年轻代GC,又选择性的回收一部分被标记的老年代,同时处理老年代与新生代。如图所示混合回收前。
JVM优化系列-JVM G1 垃圾收集器_第6张图片
混合回收之后
JVM优化系列-JVM G1 垃圾收集器_第7张图片

  会看到由于新生代GC毕然导致Eden被清空,而标记G的区域就变为了垃圾比例最高的区域,所以被清理。


17.698: [GC pause (G1 Evacuation Pause) (mixed), 0.0119168 secs]

[Eden: 34.0M(34.0M)->0.0B(259.0M) Survivors: 6144.0K->5120.0K Heap: 208.0M(800.0M)->201.0M(800.0M)]

  混合GC会执行多次,知道回收了足够多的内存,然后就会触发一次新生代GC,新生代GC之后有可能会执行一次并发标记周期,最后又会引起混合GC。

G1 的 Full GC操作

  在有些场景下会在日志里面出现了Full GC 的信息。这是因为并发收集由于让应用程序和GC线程交替工作,所以总是不能避免在特别繁忙的时候回出现回收的过程中内存不足的情况,这个时候就会进行一次的Full GC,可以使用如下的参数作为测试参数查看对应的效果

-Xmx1g -Xms1g -Xmn900m -XX:+UseG1GC -Xloggc:gc.log -XX:+PrintGCDetails

总结

  对于G1回收器,可以通过-XX:+UseG1GC 标记来打开,可以通过-XX:+MaxGCPauseMillis 用于指定目标最大停顿时间,如果任何一次的停顿大小与这个时间G1就会尝试重新调整新生代和老年的比例、调整整个堆大小、调整晋升年龄等。通过-XX:+ParallelGCThreads 设置并行回收的时候,GC工作线程数量,-XX:InitiatingHeapOccupancyPercent参数可以设置整个堆使用率达到多少的时候触发标记周期执行,默认是45。整个值一旦被设置就不会被修改,这个值配合MaxGCPauseMillis 来使用。当然还有很多的性能调优的参数,这里就不再一一介绍。

你可能感兴趣的:(JVM系列)