【JVM】垃圾回收算法、GC日志解析、内存分配担保机制、八大垃圾收集器

JVM的垃圾回收

垃圾回收一般发生在堆和方法区,也就是线程共享的区域,主要是堆

堆和方法区的内存分配,也是通过垃圾收集器去实现的

垃圾回收,也是通过垃圾收集器实现的

不同的垃圾收集器可能采用不同的垃圾收集算法,去判断对象是否是垃圾对象

垃圾回收判断算法

  • 主要是两种:引用计数法可达性算法

引用计数法

  • 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1:当引用失效时,计数器值就减1:任何时刻计数器都为0的对象就是不可能再被使用的

  • 实现简单,判断效率高,大部分情况下都是很不错的算法

  • Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的相互循环引用的问题

public class Test {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
        object1.object = object2;
        object2.object = object1;
        object1 = null;
        object2 = null;
    }
} 
class MyObject{
    MyObject object;
}

可达性算法 又名 根搜索算法

  • 在主流的商用程序语言中(Java和C#),都是使用根搜索算法判定对象是否存活的

  • 基本思路就是通过一系列的名为“GCRoots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoot没有任何引用链相连(就是从GCRoot到这个对象不可达)时,则证明此对象是不可用的

    • 不可达不一定会被回收,可以用finalize()方法抢救下,但绝对不推荐

可作GCRoots的对象(堆中引用的不能作为GCRoot对象)

  • 虚拟机栈(栈帧中的本地变量表)中的引用的对象

  • 方法区中的类静态属性引用的对象

  • 方法区中的常量引用的对象

  • 本地方法栈中JNl(即一般说的Native方法)的引用的对象

对象引用

  • 在JDK1.2之后,将引用分为四种,,强度依次减弱

    • 强引用(StrongReference)、

      • Object obj=new Object()
      • 代码中普遍存在,只要强引用还在,就不会被GC

    • 软引用(SoftReference)、

      • Object obj = new Object();
        SoftReference<Object> sf = new SoftReference<Object>(obj);
        obj = null;
        sf.get();//有时候会返回null
      • 非必须引用,内存溢出之前进行回收,如内存还不够,才会抛异常

    • 弱引用(WeakReference)、

      • Object obj = new Object();
        WeakReference<Object> wf = new WeakReference<Object>(obj);
        obj = null;
        wf.get();//有时候会返回null
        wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾
      • 非必须引用,只要有GC,就会被回收

      • 弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null

      • 弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记

    • 虚引用(PhantomReference)

      • Object obj = new Object();
        PhantomReference<Object> pf = new PhantomReference<Object>(obj);
        obj=null;
        pf.get();//永远返回null
        pf.isEnQueued();//返回是否从内存中已经删除
      • 垃圾回收时回收,无法通过引用取到对象值

      • 也称为幽灵引用或者幻影引用,它是最弱的一种引用关系

      • 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例

      • 为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知

        • 虚引用主要用于检测对象是否已经从内存中删除

    回收过程

    • 即使在可达性分析算法中不可达的对象,也并非是“非死不可”,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程

      • 第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记

      • 第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize() 方法。在 finalize() 方法中没有重新与引用链建立关联关系的,将被进行第二次标记

    • 第二次标记成功的对象将真的会被回收,如果对象在 finalize() 方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活

    方法区回收

    • 方法区也是有垃圾回收的,主要回收废弃常量和无用的类

      • 废弃常量

        • 比如字符串常量,没有对象引用即可回收

        • 常量池中的其他类(接口)、方法、字段的符号引用也与此类似

      • 无用的类(需要同时满足3个条件)

        • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例

        • 加载该类的ClassLoader已经被回收

        • 该类对应的java.lang.Class对象在任何地方没有被引用,也无法通过反射访问该类的方法

    • 即使满足回收条件也不一定真得回收。主要性价比太低

垃圾回收算法

标记-清除算法

  • 最基本的算法,主要分为标记和清除2个阶段。首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象

  • 标记和清除过程的效率都不高

  • 会产生大量不连续的内存碎片,会导致大对象可能无法分配,提前触发GC

复制回收算法

  • 将可用内存按容量划分为相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉

  • 现在商业虚拟机都是采用这种收集算法来回收新生代

    • 还记得这张图吗???

    • 当回收时,将Eden和Survivor中还存活着的对象拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor的空间

    • 当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保

标记-整理算法

  • 老年代没有人担保,不能用复制回收算法。可以用标记-整理算法

  • 标记过程仍然与“标记-清除”算法一样,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代回收算法

  • 当前商业虚拟机都是采用这种算法。根据对象的存活周期的不同将内存划分为几块

  • 新生代,每次垃圾回收都有大量对象失去,选择复制回收算法

  • 老年代,对象存活率高,无人进行分配担保,就必须采用标记清除或者标记整理算法

内存分配担保机制

新生代内存分配不够时,需要向老年代贷款(贷的是内存)。但是贷款的时候,需要有担保(抵押)

当在新生代无法分配内存的时候,把新生代的对象转移到老生代,然后把新对象放入腾空的新生代 我们的新生代分为三个区域,分别为eden space,from space和to space

分配流程说明(把Java堆大小设置为20MB,新生代和老年代各10M,不可扩展 )

  • JVM参数设置内存额度:-Xms20M、-Xmx20M、-Xmn10M

  • 然后我们通过 -XX:SurvivorRatio=8 来分配新生代各区的比例:8:1:1

  • 我们现在尝试分配三个2MB的对象和一个4MB的对象

    •  

      package com.example.demo.service;
      public class Test1 {
          private static final int _1MB = 1024 * 1024;
          public static void main(String[] args) {
              memoryAllocation();
          }
          public static void memoryAllocation() {
              byte[] allocation1, allocation2, allocation3, allocation4;
              allocation1 = new byte[2 * _1MB];
              allocation2 = new byte[2 * _1MB];
              allocation3 = new byte[2 * _1MB];
              allocation4 = new byte[4 * _1MB];
          }
      }

Client模式内存担保(了解即可,因为是Client模式,目前虚拟机无法支持,所以用的Server模式演示,别杠)

  • 这里我们先手动指定垃圾收集器为client模式下的Serial+Serial Old的收集器组合进行内存回收

  • JVM参数配置:-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC

  • 担保机制在JDK1.6之后就默认开启。这里我们使用的是JDK1.8,所以不用再手动去开启担保机制

  • 然后运行程序,看看GC日志

    • java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseSerialGC Test1

  • 通过上面的GC日志,我们可以发现发生了一次GC,让新生代从6815K变成了261K,但是整个堆的变化没什么变化

    • 那是因为前三个2MB的对象好存活着,并没有被回收

  • 我们来想想:2M * 3 + 4M = 10M

    • 我们的新生代的空间为:8M

    • 我们的S1也就是from space 的空间为:1M

    • S2和S1只会使用其中一个,故而S2空间不可使用

    • 所以整个新生代的可用空间为:8M + 1M = 9M

  • 因为 9M < 10M 所以发生了一次GC

  • 而本次GC期间,JVM发现已经存在新生代的三个 2M也无法搬运到 S1或者S2空间去(6M > 1M )

  • 于是JVM就启动了内存分配的担保机制,把这6MB的三个对象直接转移到了老年代

  • 此时新生代的空间被清理出来存放了那一个占用 4M的对象

    • 所以新生代eden space的空间占用为:51%

    • 老年代占用:60%,存放的是那三个 2M的对象

Server模式内存担保

  • 接下来我们使用服务端模式(Parallel Scavenge+Serial Old的组合)来看看担保机制的实现

  • java -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseParallelGC Test1

  • 当我们使用Server模式下的ParallelGC收集器组合(Parallel Scavenge+Serial Old的组合)下

    • 在GC前还会进行一次判断,如果要分配的内存>=Eden区大小的一半,那么会直接把要分配的内存放入老年代中。否则才会进入担保机制

  • 因为我们的新生代的空间总共10M,分代分配为 8:1:1,整个eden空间为8M

  • 我们的数据占用 4M,但是此时新生代已经被占用了6M,明显放不下这个4M了

  • 于是判断,这个4M对象是否大于整个新生代的50%空间,4M >= 8*50%结果为:true

  • 于是这个4M的对象跳过了新生代,直接进去了老年代,并没有触发GC回收

总结

JVM在内存分配的时候,新生代内存不足时,把新生代的存活的对象搬到老生代,然后新生代腾出来的空间用于为分配给最新的对象

在不同的GC机制下,也就是不同垃圾回收器组合下,担保机制也略有不同 (上面两张图已经很说明问题了)

在Serial+Serial Old的情况下,发现放不下就直接启动担保机制

在Parallel Scavenge+Serial Old的情况下,却是先要去判断一下要分配的内存是不是=Eden区大小的一半,如果是那么直接把该对象放入老生代,否则才会启动担保机制

GC日志解析

启用GC日志

-verbose:gc
-XX:+PrintGC
-Xloggc:/opt/logs/gc.log  
  • 在JDK 8中, -verbose:gc 是 -XX:+PrintGC 一个别称,日志格式等价与: -XX:+PrintGC

  • 不过在 JDK 9 中 -XX:+PrintGC 被标记为 deprecated弃用状态

    • -verbose:gc:是一个标准的选项

    • -XX:+PrintGC:是一个实验的选项,建议使用-verbose:gc替代`-XX:+PrintGC

参数详解

  • -XX:+PrintGC

    • 参数 -XX:+PrintGC (或者 -verbose:gc )开启了简单GC日志模式,为每一次新生代(younggeneration)的GC和每一次的Full GC打印一行信息

  • -XX:+PrintGCDetails

    • 开启了详细GC日志模式。在这种模式下,日志格式和所使用的GC算法有关

  • -XX:+PrintGCTimeStamps 和 -XX:+PrintGCDateStamps

    • 使用 -XX:+PrintGCTimeStamps 可以将时间和日期也加到GC日志中

    • 表示自JVM启动至今的时间戳会被添加到每一行中。例子如下

    • 0.185: [GC 66048K->53077K(251392K), 0.0977580 secs]
      0.323: [GC 119125K->114661K(317440K), 0.1448850 secs]
      0.603: [GC 246757K->243133K(375296K), 0.2860800 secs]
    • 如果指定了 -XX:+PrintGCDateStamps ,每一行就添加上了绝对的日期和时间

    • 2019-06-03T12:08:38.102-0100: [GC 66048K->53077K(251392K), 0.0959470secs]
      2019-06-03T12:08:38.239-0100: [GC 119125K->114661K(317440K), 0.1421720secs]
      2019-06-03T12:08:38.513-0100: [GC 246757K->243133K(375296K), 0.2761000secs]
    • 推荐同时使用这两个参数,因为这样在关联不同来源的GC日志时很有帮助

  • -Xloggc

    • 缺省的GC日志时输出到终端的,使用-Xloggc:也可以输出到指定的文件

    • 需要注意这个参数隐式的设置了参数 -XX:+PrintGC 和 -XX:+PrintGCTimeStamps

    • 但为了以防在新版本的JVM中有任何变化,我仍建议显示的设置这些参数

GC日志解析

Full GC

分别是排列了:年轻代GC信息、年老代GC信息、堆的GC信息、方法区GC信息

  • 分别展示上述分区的:回收前大小 -> 回收后大小 -> 所属内存大小

  • 如下所示

GC方式

minorGC

  • 新生代的垃圾回收,很快就回收了,新生代回收的频率高

  • 使用的是一些垃圾回收算法(复制算法)比较快的垃圾收集器

majorGC

  • 老年代的垃圾回收,效率比minorGC慢10倍,所以避免发生majorGC

  • 很多时候majorGC和FullGC经常混为一谈。因为一般来说发生了majorGC前,先要来一次minorGC

  • 一般来说,内存担保前发生了一次minorGC,而如果内存担保发生之后,发现老年代不够用,相当于会触发majorGC

  • 但是注意:majorGC只是针对堆,而fullGC不只是针对堆,还会针对方法区

fullGC (安全点,STW )

  • 整个堆(minorGC和majorGC)和方法区的垃圾回收

  • System.gc();不一定能立即触发fullGC

  • 老年代不够用,没人替它担保,所以会触发fullGC

  • 方法区不够用,没人替他担保,也会触发fullGC

  • 当新生代的对象无法被老年代担保成功时,也会触发fullGC

垃圾收集器

在上面我们说了垃圾回收的判断算法、垃圾的回收算法、现在来说垃圾收集器了

为了达到最大性能,基于分代管理和回收算法,结合回收的时机,JVM实现垃圾回收器

  • 串行回收

  • 并行回收

  • 并发标记回收(CMS)

  • 垃圾优先回收(G1)

我们来看看这些名词的解释,方便我们后面的学习

STW:是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起

并行

  • 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态

  • 如ParNew、Parallel Scavenge、Parallel Old;

并发

  • 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行)

  • 用户程序在继续运行,而垃圾收集程序线程运行于另一个CPU上;

吞吐量

  • 垃圾收集时间/(程序运行时间+垃圾收集时间)

串行回收:Serial

  • 串行回收使用单线程进行垃圾回收,在回收的时候应用程序需要STW

  • 新生代通常采用复制算法,老年代通常采用标记压缩算法

并行回收

  • 并行回收使用多线程进行垃圾回收,在回收的时候应用程序需要STW

  • 新生代通常采用复制算法,老年代通常采用标记压缩算法

并发标记回收CMS(老年代)

  • 并发标记回收(CMS)的整个回收期划分为多个阶段 :初始标记、并发标记、重新标记、并发清除等

  • 在初始标记和重新标记阶段需要暂停应用程序,在并发标记和并发清除期间可以和应用程序并发运行

  • 这个算法通常适用于老年代,新生代可以采用并行回收

垃圾优先回收G1

  • jdk 1.7 开始提供使用,G1致力于在多CPU和大内存服务器上对垃圾回收提供软实时目标和高吞吐量

  • G1垃圾收集器的设计和前面提到的三种收集器都不一样

  • 它在并行、串行以及CMS GC针对堆空间的管理方式上都是连续的

不需要等到垃圾满了再回收,会根据一些信息去弹性回收

  • 连续的内存将导致垃圾回收时收集时间过长,停顿时间不可控。因此G1将堆拆成一系列的分区(HeapRegion),这样在一个时间段内,大部分的垃圾收集器操作只针对一部分分区,而不是整个堆或者整个老年代

总结

  • 垃圾收集器是垃圾回收算法(标记-清除算法、复制算法、标记-整理算法、火车算法)的具体实现

  • 不同种类JVM所提供的垃圾收集器可能会有很大差别

  • HotSpot虚拟机中的8种垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1、ZGC

  • 按照回收区域来划分,分表是年轻代的垃圾收集器和年老代的垃圾收集器。

  • 按照收集方式来划分,分表是串行回收、并行回收、并发标记回收和垃圾优先回收

新生代与老年代收集器的对应关系

参数 新生代垃圾收集器 新生代 算法 老年代垃圾收集器老 年 代 算 法
-XX:+UseSerialGC SerialGC 复制 SerialOldGC 标 记 整 理
-XX:+UseParNewGC ParNewGC 复制 SerialOldGC 标 记 整 理
-XX:+UseParallelGC Parallel[Scavenge] 复制 ParallelOldGC 标 记 整 理
- XX:+UseConcMarkSweepGC ParNew 复制 CMS+SerialOld的 收集器组合 (Serial Old作 为CMS出错的后备 收集器) 标 记 清 除
-XX:+UseG1GC G1(整体上采用标记- 整理算法) 局部是通过复制算法,不会产生内存碎片    

Serial收集器(新生代)

回收的内存区域是年轻代,垃圾收集方式是串行(单线程)方式。采用的回收算法是复制算法 串行收集器组合 Serial + Serial Old;

Serial收集器是最基本、发展历史最悠久的收集器,它在JDK1.3.1前是HotSpot新生代收集的唯一选择; 依然是HotSpot在Client模式下默认的新生代收集器

开启:-XX:+UseSerialGC

应用场景

  • 简单高效(与其他收集器的单线程相比)

  • 对单个CPU环境,Serial收集器没有线程切换开销,可获得最高的单线程收集效率;

  • 在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这是可以接受的

Serial Old收集器(老年代)

Serial Old收集器是 Serial收集器的老年代版本

回收的内存区域是老年代,垃圾收集方式是串行(单线程)方式。采用的回收算法是标记清除压缩算法

应用场景

  • 主要用于Client模式

  • 而在Server模式的用途:在JDK1.5及之前,与Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配)

ParNew收集器(新生代)

回收的内存区域是新生代,垃圾收集方式是并行(多线程)方式。采用的回收算法是复制算法 ParNew收集器是Serial收集器的多线程版本 进行垃圾收集时会停止所有的用户线程

设置参数:

  • “-XX:+UseConcMarkSweepGC”:指定使用CMS后,会默认使用ParNew作为新生代收集器

  • “-XX:+UseParNewGC”:强制指定使用ParNew

  • “-XX:ParallelGCThreads”:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同

应用场景

  • 在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作

  • 但在单个CPU环境中,不会比Serail收集器有更好的效果,因为存在线程交互开销

为什么只有ParNew能与CMS收集器配合?

  • CMS是HotSpot在JDK1.5第一款并发收集器,让垃圾收集线程与用户线程(基本上)同时工作

  • CMS作为老年代收集器,但无法与JDK1.4新生代收集器Parallel Scavenge配合工作

  • 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码

Parallel Scavenge收集器(新生代)

回收的内存区域是新生代,垃圾收集方式是并行(多线程)方式。采用的回收算法是复制算法 } 因为与吞吐量关系密切,也称为吞吐量收集器(Throughput Collector) 并行收集器组合 Parallel Scavenge + Parallel Old

设置参数

  • -XX:+UseParallelGC或-XX:+UseParallelOldGC(可互相激活)

  • “-XX:MaxGCPauseMillis”:控制最大垃圾收集停顿时间

    • 置得稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降,因为可能导致垃圾收集发生得更频繁

  • “-XX:GCTimeRatio”:设置垃圾收集时间占总时间的比率

    • 0<n<100的整数,GCTimeRatio相当于设置吞吐量大小

  • “-XX:+UseAdptiveSizePolicy”: 开启这个参数后,就不用手工指定一些细节参数

    • JVM会根据当前系统运行情况动态调整这些参数,这种调节方式称为GC自适应的调节策略

      • 新生代的大小(-Xmn)

      • Eden与Survivor区的比例(-XX:SurvivorRation)

      • 晋升老年代的对象年龄(-XX:PretenureSizeThreshold)

      • ......

并行收集器是以关注吞吐量为目标的垃圾收集器,也是server模式下的默认收集器配置,对吞吐量的关注主要体现在年轻代Parallel Scavenge收集器

  • CMS等收集器的关注点是缩短垃圾收集时用户线程的停顿时间

  • 而Parallel Scavenge收集器的目标则是达一个可控制的吞吐量(Throughput)

应用场景

  • 提高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间

  • 适用于多CPU,对停顿时间没特别要求的后台统计等运算任务

Parallel Old收集器(老年代)

回收的内存区域是年老代,垃圾收集方式是并行(多线程)方式。采用的回收算法是标记整理算法 Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本 并行收集器组合 Parallel Scavenge + Parallel Old

应用场景

  • JDK1.6及之后用来代替老年代的Serial Old收集器

  • 特别是在Server模式,多CPU的情况下

  • 在注重吞吐量以及CPU资源敏感场景,就有Parallel Scavenge加Parallel Old收集器"给力"组合

CMS收集器(老年代)

回收的内存区域是老年代,垃圾收集方式是并行(多线程)方式 采用的回收算法是标记清除(不进行压缩操作,产生内存碎片)算法

配置参数:

  • -XX:+UseConcMarkSweepGC 指定使用CMS收集器

  • -XX:+UseCMSCompactAtFullCollection 强制进行空间碎片整理

    • CMS 采用标记清除算法,会产生大量的空间碎片,以上操作会引发STW

  • -XX:+CMSFullGCsBeforeCompaction 配置经过几次的FullGC进行空间碎片整理

  • -XX:+CMSFullGCsBeforeCompaction=10 经过10次FGC后进行空间碎片整理,以降低STW次数

CMS是一款优秀的收集器,主要优点:并发收集、低停顿。

CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低

CMS收集器无法处理浮动垃圾,可能会出现“Concurrent Mode Failure(并发模式故障)”失败而导致Full GC产生

  • 由于CMS并发清理阶段用户线程还在运行着,伴随着程序运行自然就会有新的垃圾不断产生,这部分垃圾出现的标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC中再清理。这些垃圾就是“浮动垃圾”

CMS是一款“标记--清除”算法实现的收集器,容易出现大量空间碎片,当空间碎片过多,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC

应用场景

  • 与用户交互较多的场景;希望系统停顿时间最短,注重服务的响应速度

  • 以给用户带来较好的体验;如常见WEB、B/S系统的服务器上的应用

G1收集器(整个堆)

回收的内存区域是整个JVM,垃圾收集方式是并行(多线程)方式。在物理内存中没有真正去采用分代的方式去划分内存

分区可以逻辑上被用来作为年轻代内存区域和年老代内存区域

采用的回收算法是复制算法和标记清除标记整理算法

在JDK9之前,JDK7和JDK8默认都是ParallelGC垃圾回收。到了JDK9,G1才是默认的垃圾回收器

配置参数:

  • -XX:+UseG1GC JDK7和JDK8开启

"首先收集尽可能多的垃圾(Garbage First)",G1并不会等内存耗尽(串行、并行)或者快耗尽(CMS)的时候开始垃圾收集,而是在内部采用了启发式算法,在老年代找出具有高收集收益的分区进行收集

  • G1整个堆空间分成若干个大小相等的内存区域:Regions

  • 默认将整堆划分为2048个分区,可以通过参数-XX:G1HeapRegionSize=n可指定分区大小

  • 在上图中可以看出,G1可以分为四个类型的分区

  • 它依然是有年轻代和年老代,年轻代中有伊甸园区和两个幸存区

  • 我们发现还有一个Humongous:

    • 当一个对象大于Region大小的50%,称为巨型对象;它就会独占一个或多个Region

    • 巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区-Humongous Region

而在分区中又有一个概念出来:Card

  • 很小的内存区域 ,大小为512 Byte

  • Card Table维护着所有的Card。Card Table的结构是一个字节数组,Card Table用这个数组映射着每一个Card

  • Card中对象的引用发生改变时,Card在Card Table数组中对应的值被标记为dirty,就称这个Card被脏化了

  • 分配对象会占用物理上连续若干个卡片

G1保留了分代的概念,但是年轻代和年老代不再是物理上的隔离,他们都是一部分的Regions(不需要连续)的集合,每个Region都可能随G1的运行在不同代之间切换

  • 年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲Region加入到年轻代空间

  • 整个年轻代内存会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认60%)之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis(默认200ms)、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到

  • 当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio、-Xmn),但同时暂停目标将失去意义

Card Table 和Remember Set

  • Remembered Sets, 用来记录外部指向本Region的所有引用,每个Region维护一个RSet。

  • Card:JVM将内存划分成了固定大小的Card。这里可以类比物理内存上page的概念,每个Region被分成了多个Card

  • G1将垃圾收集和内存整理活动专注于那些几乎全是垃圾的区域,并建立停顿预测模型来决定每次GC时回收哪些区域,以满足用户设定的停顿时间

G1 Young GC

Young GC主要是对Eden区进行GC,它在Eden空间耗尽时会被触发。在这种情况下,Eden空间的数据移动到Survivor空间中,如果Survivor空间不够,Eden空间的部分数据会直接晋升到年老代空间。Survivor区的数据移动到新的Survivor区中,也有部分数据晋升到老年代空间中。最终Eden空间的数据为空,GC停止工作,应用线程继续执行

这时,我们需要考虑一个问题,如果仅仅GC 新生代对象,我们如何找到所有的根对象呢? 老年代的所有对象都是根么?那这样扫描下来会耗费大量的时间。于是,G1引进了RSet的概念。它的全称是Remembered Set,作用是跟踪指向某个heap区内的对象引用

G1 Mix GC

  • Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区

posted @ 2019-08-08 16:36  鞋破露脚尖儿  阅读(725)  评论(0编辑  收藏  举报