看完这篇文章,我不再怕面试官问「垃圾回收」了...

前言

Java 相比 C/C++ 最显著的特色即是引入了自动垃圾回收 (下文统一用 GC 指代自动垃圾回收),它解决了 C/C++ 最使人头疼的内存管理问题,让程序员专一于程序自己,不用关心内存回收这些恼人的问题,这也是 Java 能大行其道的重要缘由之一,GC 真正让程序员的生产力获得了释放,可是程序员很难感知到它的存在,这就比如,咱们吃完饭后在桌上放下餐盘即走,服务员会替你收拾好这些餐盘,你不会关心服务员何时来收,怎么收。html

有人说既然 GC 已经自动咱们完成了清理,不了解 GC 貌似也没啥问题。在大多数状况下确实没问题,不过若是涉及到一些性能调优,问题排查等,深刻地了解 GC 仍是必不可少的,曾经美团经过调整 JVM 相关 GC 参数让服务响应时间 TP90,TP99都降低了10ms+,服务可用性获得了很大的提高!因此深刻了解 GC 是成为一名优秀 Java 程序员的必修课!java

垃圾回收分上下篇,上篇会先讲垃圾回收理论,主要包括程序员

  1. GC 的几种主要的收集方法:标记清除、标记整理、复制算法的原理与特色,各自的优劣势
  2. 为啥会有 Serial ,CMS, G1 等各式样的回收器,各自的优劣势是什么,为啥没有一个统一的万能的垃圾回收器
  3. 新生代为啥要设置成 Eden, S0,S1 这三个区,基于什么考虑呢
  4. 堆外内存不受 GC 控制,那该怎么释放呢
  5. 对象可回收,就必定会被回收吗?
  6. 什么是 SafePoint,什么是 Stop The World

下篇主要讲垃圾回收的实践,主要包括web

  1. GC 日志格式怎么看
  2. 主要有哪些发生 OOM 的场景
  3. 发生 OOM,如何定位,经常使用的内存调试工具备哪些

本文会从如下几方面来阐述垃圾回收算法

  1. JVM 内存区域
  2. 如何识别垃圾
    • 引用计数法
    • 可达性算法
  3. 垃圾回收主要方法
    • 标记清除法
    • 复制法
    • 标记整理法
    • 分代收集算法
  4. 垃圾回收器对比

文字比较多,不过也为了便于读者理解加了很多 GC 的动画,相信看完会有很多收获c#

JVM 内存区域

要搞懂垃圾回收的机制,咱们首先要知道垃圾回收主要回收的是哪些数据,这些数据主要在哪一块区域,因此咱们一块儿来看下 JVM 的内存区域数组

  • 虚拟机栈:描述的是方法执行时的内存模型,是线程私有的,生命周期与线程相同,每一个方法被执行的同时会建立栈桢(下文会看到),主要保存执行方法时的局部变量表、操做数栈、动态链接和方法返回地址等信息,方法执行时入栈,方法执行完出栈,出栈就至关于清空了数据,入栈出栈的时机很明确,因此这块区域不须要进行 GC缓存

  • 本地方法栈:与虚拟机栈功能很是相似,主要区别在于虚拟机栈为虚拟机执行 Java 方法时服务,而本地方法栈为虚拟机执行本地方法时服务的。这块区域也不须要进行 GC安全

  • 程序计数器:线程独有的, 能够把它看做是当前线程执行的字节码的行号指示器,好比以下字节码内容,在每一个字节码`前面都有一个数字(行号),咱们能够认为它就是程序计数器存储的内容记录这些数字(指令地址)有啥用呢,咱们知道 Java 虚拟机的多线程是经过线程轮流切换并分配处理器的时间来完成的,在任何一个时刻,一个处理器只会执行一个线程,若是这个线程被分配的时间片执行完了(线程被挂起),处理器会切换到另一个线程执行,当下次轮到执行被挂起的线程(唤醒线程)时,怎么知道上次执行到哪了呢,经过记录在程序计数器中的行号指示器便可知道,因此程序计数器的主要做用是记录线程运行时的状态,方便线程被唤醒时能从上一次被挂起时的状态继续执行,须要注意的是,程序计数器是惟一一个在 Java 虚拟机规范中没有规定任何 OOM 状况的区域,因此这块区域也不须要进行 GC微信

  • 本地内存:线程共享区域,Java 8 中,本地内存,也是咱们一般说的堆外内存,包含元空间和直接内存,注意到上图中 Java 8 和 Java 8 以前的 JVM 内存区域的区别了吗,在 Java 8 以前有个永久代的概念,实际上指的是 HotSpot 虚拟机上的永久代,它用永久代实现了 JVM 规范定义的方法区功能,主要存储类的信息,常量,静态变量,即时编译器编译后代码等,这部分因为是在堆中实现的,受 GC 的管理,不过因为永久代有 -XX:MaxPermSize 的上限,因此若是动态生成类(将类信息放入永久代)或大量地执行 String.intern (将字段串放入永久代中的常量区),很容易形成 OOM,有人说能够把永久代设置得足够大,但很难肯定一个合适的大小,受类数量,常量数量的多少影响很大。因此在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也所以提高了性能(发生 GC 会发生 Stop The Word,形成性能受到必定影响,后文会提到),也就不存在因为永久代限制大小而致使的 OOM 异常了(假设总内存1G,JVM 被分配内存 100M, 理论上元空间能够分配 2G-100M = 1.9G,空间大小足够),也方便在元空间中统一管理。综上所述,在 Java 8 之后这一区域也不须要进行 GC

        画外音: 思考一个问题,堆外内存不受 GC控制,没法经过 GC 释放内存,那该以什么样的形式释放呢,总不能只建立不释放吧,这样的话内存可能很快就满了,这里不作详细阐述,请看文末的参考文章

  • 堆:前面几块数据区域都不进行 GC,那只剩下堆了,是的,这里是 GC 发生的区域!对象实例和数组都是在堆上分配的,GC 也主要对这两类数据进行回收,这块也是咱们以后重点须要分析的区域

如何识别垃圾

上一节咱们详细讲述了 JVM 的内存区域,知道了 GC 主要发生在堆,那么 GC 该怎么判断堆中的对象实例或数据是否是垃圾呢,或者说判断某些数据是不是垃圾的方法有哪些。

引用计数法

最容易想到的一种方式是引用计数法,啥叫引用计数法,简单地说,就是对象被引用一次,在它的对象头上加一次引用次数,若是没有被引用(引用次数为 0),则此对象可回收

String ref = new String("Java");

以上代码 ref1 引用了右侧定义的对象,因此引用次数是 1

若是在上述代码后面添加一个 ref = null,则因为对象没被引用,引用次数置为 0,因为不被任何变量引用,此时即被回收,动图以下

看起来用引用计数确实没啥问题了,不过它没法解决一个主要的问题:循环引用!啥叫循环引用

public  class TestRC {

    TestRC instance;
public TestRC(String name) {
    }

public static void main(String[] args) {
// 第一步
A a = new TestRC("a");
B b = new TestRC("b");

// 第二步
a.instance = b;
b.instance = a;

// 第三步
a = null;
b = null;
}
}

按步骤一步步画图

到了第三步,虽然 a,b 都被置为 null 了,可是因为以前它们指向的对象互相指向了对方(引用计数都为 1),因此没法回收,也正是因为没法解决循环引用的问题,因此现代虚拟机都不用引用计数法来判断对象是否应该被回收。

可达性算法

现代虚拟机基本都是采用这种算法来判断对象是否存活,可达性算法的原理是以一系列叫作  GC Root  的对象为起点出发,引出它们指向的下一个节点,再如下个节点为起点,引出此节点指向的下一个结点。。。(这样经过 GC Root 串成的一条线就叫引用链),直到全部的结点都遍历完毕,若是相关对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为「垃圾」,会被 GC 回收。

如图示,若是用可达性算法便可解决上述循环引用的问题,由于从GC Root 出发没有到达 a,b,因此 a,b 可回收

a, b 对象可回收,就必定会被回收吗?并非,对象的 finalize 方法给了对象一次垂死挣扎的机会,当对象不可达(可回收)时,当发生GC时,会先判断对象是否执行了 finalize 方法,若是未执行,则会先执行 finalize 方法,咱们能够在此方法里将当前对象与 GC Roots 关联,这样执行 finalize 方法以后,GC 会再次判断对象是否可达,若是不可达,则会被回收,若是可达,则不回收!

注意: finalize 方法只会被执行一次,若是第一次执行 finalize 方法此对象变成了可达确实不会回收,但若是对象再次被 GC,则会忽略 finalize 方法,对象会被回收!这一点切记!

那么这些 GC Roots 究竟是什么东西呢,哪些对象能够做为 GC Root 呢,有如下几类

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即通常说的 Native 方法)引用的对象

虚拟机栈中引用的对象

以下代码所示,a 是栈帧中的本地变量,当 a = null 时,因为此时 a 充当了 GC Root 的做用,a 与原来指向的实例 new Test() 断开了链接,因此对象会被回收。

public class Test {
public static void main(String[] args) {
Test a = new Test();
a = null;
}
}

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

以下代码所示,当栈帧中的本地变量 a = null 时,因为 a 原来指向的对象与 GC Root (变量 a) 断开了链接,因此 a 原来指向的对象会被回收,而因为咱们给 s 赋值了变量的引用,s 在此时是类静态属性引用,充当了 GC Root 的做用,它指向的对象依然存活!

public  class Test {
    public static Test s;
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}

方法区中常量引用的对象

以下代码所示,常量 s 指向的对象并不会由于 a 指向的对象被回收而回收

public  class Test {
public static final Test s = new Test();
    public static void main(String[] args) {
    Test a = new Test();
    a = null;
    }
}

本地方法栈中 JNI 引用的对象

这是简单给不清楚本地方法为什么物的童鞋简单解释一下:所谓本地方法就是一个 java 调用非 java 代码的接口,该方法并不是 Java 实现的,可能由 C 或 Python等其余语言实现的, Java 经过 JNI 来调用本地方法, 而本地方法是以库文件的形式存放的(在 WINDOWS 平台上是 DLL 文件形式,在 UNIX 机器上是 SO 文件形式)。经过调用本地的库文件的内部方法,使 JAVA 能够实现和本地机器的紧密联系,调用系统级的各接口方法,仍是不明白?见文末参考,对本地方法定义与使用有详细介绍。

当调用 Java 方法时,虚拟机会建立一个栈桢并压入 Java 栈,而当它调用的是本地方法时,虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态链接并直接调用指定的本地方法。

JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
// 缓存String的class
jclass jc = (*env)->FindClass(env, STRING_PATH);
}

如上代码所示,当 java 调用以上本地方法时,jc 会被本地方法栈压入栈中, jc 就是咱们说的本地方法栈中 JNI 的对象引用,所以只会在此本地方法执行完成后才会被释放。

垃圾回收主要方法

上一节咱们知道了能够经过可达性算法来识别哪些数据是垃圾,那该怎么对这些垃圾进行回收呢。主要有如下几种方式方式

  • 标记清除算法
  • 复制算法
  • 标记整理法

标记清除算法

步骤很简单

  1. 先根据可达性算法 标记出相应的可回收对象(图中黄色部分)
  2. 对可回收的对象进行回收 操做起来确实很简单,也不用作移动数据的操做,那有啥问题呢?仔细看上图,没错,内存碎片!假如咱们想在上图中的堆中分配一块须要 连续内存占用 4M 或 5M 的区域,显然是会失败,怎么解决呢,若是能把上面未使用的 2M, 2M,1M 内存能连起来就能连成一片可用空间为 5M 的区域便可,怎么作呢?

复制算法

把堆等分红两块区域, A 和 B,区域 A 负责分配对象,区域 B 不分配, 对区域 A 使用以上所说的标记法把存活的对象标记出来(下图有误无需清除),而后把区域 A 中存活的对象都复制到区域 B(存活对象都依次紧邻排列)最后把 A 区对象所有清理掉释放出空间,这样就解决了内存碎片的问题了。

不过复制算法的缺点很明显,好比给堆分配了 500M 内存,结果只有 250M 可用,空间无缘无故减小了一半!这确定是不能接受的!另外每次回收也要把存活对象移动到另外一半,效率低下(咱们能够想一想删除数组元素再把非删除的元素往一端移,效率显然堪忧)

标记整理法

前面两步和标记清除法同样,不一样的是它在标记清除法的基础上添加了一个整理的过程 ,即将全部的存活对象都往一端移动,紧邻排列(如图示),再清理掉另外一端的全部区域,这样的话就解决了内存碎片的问题。

可是缺点也很明显:每进一次垃圾清除都要频繁地移动存活的对象,效率十分低下。

分代收集算法

分代收集算法整合了以上算法,综合了这些算法的优势,最大程度避免了它们的缺点,因此是现代虚拟机采用的首选算法,与其说它是算法,倒不是说它是一种策略,由于它是把上述几种算法整合在了一块儿,为啥须要分代收集呢,来看一下对象的分配有啥规律如图示:纵轴表明已分配的字节,而横轴表明程序运行时间

由图可知,大部分的对象都很短命,都在很短的时间内都被回收了(IBM 专业研究代表,通常来讲,98% 的对象都是朝生夕死的,通过一次 Minor GC 后就会被回收),因此分代收集算法根据对象存活周期的不一样将堆分红新生代和老生代(Java8之前还有个永久代),默认比例为 1 : 2,新生代又分为 Eden 区, from Survivor 区(简称S0),to Survivor 区(简称 S1),三者的比例为 8: 1 : 1,这样就能够根据新老生代的特色选择最合适的垃圾回收算法,咱们把新生代发生的 GC 称为 Young GC(也叫 Minor GC),老年代发生的 GC 称为 Old GC(也称为 Full GC)。

画外音:思考一下,新生代为啥要分这么多区?

那么分代垃圾收集是怎么工做的呢,咱们一块儿来看看

分代收集工做原理

一、对象在新生代的分配与回收

由以上的分析可知,大部分对象在很短的时间内都会被回收,对象通常分配在 Eden 区

当 Eden 区将满时,触发 Minor GC

咱们以前怎么说来着,大部分对象在短期内都会被回收, 因此通过 Minor GC 后只有少部分对象会存活,它们会被移到 S0 区(这就是为啥空间大小  Eden: S0: S1 = 8:1:1, Eden 区远大于 S0,S1 的缘由,由于在 Eden 区触发的 Minor GC 把大部对象(接近98%)都回收了,只留下少许存活的对象,此时把它们移到 S0 或 S1 绰绰有余)同时对象年龄加一(对象的年龄即发生 Minor GC 的次数),最后把 Eden 区对象所有清理以释放出空间,动图以下

当触发下一次 Minor GC 时,会把 Eden 区的存活对象和 S0(或S1) 中的存活对象(S0 或 S1 中的存活对象通过每次 Minor GC 均可能被回收)一块儿移到 S1(Eden 和 S0 的存活对象年龄+1), 同时清空 Eden 和 S0 的空间。

若再触发下一次 Minor GC,则重复上一步,只不过此时变成了 从 Eden,S1 区将存活对象复制到 S0 区,每次垃圾回收, S0, S1 角色互换,都是从 Eden ,S0(或S1) 将存活对象移动到 S1(或S0)。也就是说在 Eden 区的垃圾回收咱们采用的是复制算法,由于在 Eden 区分配的对象大部分在 Minor GC 后都消亡了,只剩下极少部分存活对象(这也是为啥 Eden:S0:S1 默认为 8:1:1 的缘由),S0,S1 区域也比较小,因此最大限度地下降了复制算法形成的对象频繁拷贝带来的开销。

二、对象什么时候晋升老年代

  • 当对象的年龄达到了咱们设定的阈值,则会从S0(或S1)晋升到老年代如图示:年龄阈值设置为 15, 当发生下一次 Minor GC 时,S0 中有个对象年龄达到 15,达到咱们的设定阈值,晋升到老年代!

  • 大对象 当某个对象分配须要大量的连续内存时,此时对象的建立不会分配在 Eden 区,会直接分配在老年代,由于若是把大对象分配在 Eden 区, Minor GC 后再移动到 S0,S1 会有很大的开销(对象比较大,复制会比较慢,也占空间),也很快会占满 S0,S1 区,因此干脆就直接移到老年代.

  • 还有一种状况也会让对象晋升到老年代,即在 S0(或S1) 区相同年龄的对象大小之和大于 S0(或S1)空间一半以上时,则年龄大于等于该年龄的对象也会晋升到老年代。

三、空间分配担保

在发生 MinorGC 以前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代全部对象的总空间,若是大于,那么Minor GC 能够确保是安全的,若是不大于,那么虚拟机会查看 HandlePromotionFailure 设置值是否容许担保失败。若是容许,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代对象的平均大小,若是大于则进行 Minor GC,不然可能进行一次 Full GC。

四、Stop The World

若是老年代满了,会触发 Full GC, Full GC 会同时回收新生代和老年代(即对整个堆进行GC),它会致使 Stop The World(简称 STW),形成挺大的性能开销。

什么是 STW ?所谓的 STW, 即在 GC(minor GC 或 Full GC)期间,只有垃圾回收器线程在工做,其余工做线程则被挂起。

画外音:为啥在垃圾收集期间其余工做线程会被挂起?想象一下,你一边在收垃圾,另一群人一边丢垃圾,垃圾能收拾干净吗。

通常 Full GC 会致使工做线程停顿时间过长(由于Full GC 会清理整个堆中的不可用对象,通常要花较长的时间),若是在此 server 收到了不少请求,则会被拒绝服务!因此咱们要尽可能减小 Full GC(Minor GC 也会形成 STW,但只会触发轻微的 STW,由于 Eden 区的对象大部分都被回收了,只有极少数存活对象会经过复制算法转移到 S0 或 S1 区,因此相对还好)。

如今咱们应该明白把新生代设置成 Eden, S0,S1区或者给对象设置年龄阈值或者默认把新生代与老年代的空间大小设置成 1:2 都是为了尽量地避免对象过早地进入老年代,尽量晚地触发 Full GC。想一想新生代若是只设置 Eden 会发生什么,后果就是每通过一次 Minor GC,存活对象会过早地进入老年代,那么老年代很快就会装满,很快会触发 Full GC,而对象其实在通过两三次的 Minor GC 后大部分都会消亡,因此有了 S0,S1的缓冲,只有少数的对象会进入老年代,老年代大小也就不会这么快地增加,也就避免了过早地触发 Full GC。

因为 Full GC(或Minor GC) 会影响性能,因此咱们要在一个合适的时间点发起 GC,这个时间点被称为 Safe Point,这个时间点的选定既不能太少以让 GC 时间太长致使程序过长时间卡顿,也不能过于频繁以致于过度增大运行时的负荷。通常当线程在这个时间点上状态是能够肯定的,如肯定 GC Root 的信息等,可使 JVM 开始安全地 GC。Safe Point 主要指的是如下特定位置:

  • 循环的末尾
  • 方法返回前
  • 调用方法的 call 以后
  • 抛出异常的位置 另外须要注意的是因为新生代的特色(大部分对象通过 Minor GC后会消亡), Minor GC 用的是复制算法,而在老生代因为对象比较多,占用的空间较大,使用复制算法会有较大开销(复制算法在对象存活率较高时要进行屡次复制操做,同时浪费一半空间)因此根据老生代特色,在老年代进行的 GC 通常采用的是标记整理法来进行回收。

垃圾收集器种类

若是说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 虚拟机规范并无规定垃圾收集器应该如何实现,所以通常来讲不一样厂商,不一样版本的虚拟机提供的垃圾收集器实现可能会有差异,通常会给出参数来让用户根据应用的特色来组合各个年代使用的收集器,主要有如下垃圾收集器

  • 在新生代工做的垃圾回收器:Serial, ParNew, ParallelScavenge
  • 在老年代工做的垃圾回收器:CMS,Serial Old, Parallel Old
  • 同时在新老生代工做的垃圾回收器:G1

图片中的垃圾收集器若是存在连线,则表明它们之间能够配合使用,接下来咱们来看看各个垃圾收集器的具体功能。

新生代收集器

Serial 收集器

Serial 收集器是工做在新生代的,单线程的垃圾收集器,单线程意味着它只会使用一个 CPU 或一个收集线程来完成垃圾回收,不只如此,还记得咱们上文提到的 STW 了吗,它在进行垃圾收集时,其余用户线程会暂停,直到垃圾收集结束,也就是说在 GC 期间,此时的应用不可用。

看起来单线程垃圾收集器不太实用,不过咱们须要知道的任何技术的使用都不能脱离场景,在 Client 模式下,它简单有效(与其余收集器的单线程比),对于限定单个 CPU 的环境来讲,Serial 单线程模式无需与其余线程交互,减小了开销,专心作 GC 能将其单线程的优点发挥到极致,另外在用户的桌面应用场景,分配给虚拟机的内存通常不会很大,收集几十甚至一两百兆(仅是新生代的内存,桌面应用基本不会再大了),STW 时间能够控制在一百多毫秒内,只要不是频繁发生,这点停顿是能够接受的,因此对于运行在 Client 模式下的虚拟机,Serial 收集器是新生代的默认收集器

ParNew 收集器

ParNew 收集器是 Serial 收集器的多线程版本,除了使用多线程,其余像收集算法,STW,对象分配规则,回收策略与 Serial 收集器完成同样,在底层上,这两种收集器也共用了至关多的代码,它的垃圾收集过程以下

ParNew 主要工做在 Server 模式,咱们知道服务端若是接收的请求多了,响应时间就很重要了,多线程可让垃圾回收得更快,也就是减小了 STW 时间,能提高响应时间,因此是许多运行在 Server 模式下的虚拟机的首选新生代收集器,另外一个与性能无关的缘由是由于除了 Serial  收集器,只有它能与 CMS 收集器配合工做,CMS 是一个划时代的垃圾收集器,是真正意义上的并发收集器,它第一次实现了垃圾收集线程与用户线程(基本上)同时工做,它采用的是传统的 GC 收集器代码框架,与 Serial,ParNew 共用一套代码框架,因此能与这二者一块儿配合工做,然后文提到的 Parallel Scavenge 与 G1 收集器没有使用传统的 GC 收集器代码框架,而是另起炉灶独立实现的,另一些收集器则只是共用了部分的框架代码,因此没法与 CMS 收集器一块儿配合工做。

在多 CPU 的状况下,因为 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快,也能有效地减小 STW 的时间,提高应用的响应速度。

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一个使用复制算法多线程,工做于新生代的垃圾收集器,看起来功能和 ParNew 收集器同样,它有啥特别之处吗

关注点不一样,CMS 等垃圾收集器关注的是尽量缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)),也就是说 CMS 等垃圾收集器更适合用到与用户交互的程序,由于停顿时间越短,用户体验越好,而 Parallel Scavenge 收集器关注的是吞吐量,因此更适合作后台运算等不须要太多用户交互的任务。

Parallel Scavenge 收集器提供了两个参数来精确控制吞吐量,分别是控制最大垃圾收集时间的 -XX:MaxGCPauseMillis 参数及直接设置吞吐量大小的 -XX:GCTimeRatio(默认99%)

除了以上两个参数,还能够用 Parallel Scavenge 收集器提供的第三个参数 -XX:UseAdaptiveSizePolicy,开启这个参数后,就不须要手工指定新生代大小,Eden 与 Survivor 比例(SurvivorRatio)等细节,只须要设置好基本的堆大小(-Xmx 设置最大堆),以及最大垃圾收集时间与吞吐量大小,虚拟机就会根据当前系统运行状况收集监控信息,动态调整这些参数以尽量地达到咱们设定的最大垃圾收集时间或吞吐量大小这两个指标。自适应策略也是 Parallel Scavenge  与 ParNew 的重要区别!

老年代收集器

Serial Old 收集器

上文咱们知道, Serial 收集器是工做于新生代的单线程收集器,与之相对地,Serial Old 是工做于老年代的单线程收集器,此收集器的主要意义在于给 Client 模式下的虚拟机使用,若是在 Server 模式下,则它还有两大用途:一种是在 JDK 1.5 及以前的版本中与 Parallel Scavenge 配合使用,另外一种是做为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用(后文讲述),它与 Serial 收集器配合使用示意图以下

Parallel Old 收集器

Parallel Old 是相对于 Parallel Scavenge 收集器的老年代版本,使用多线程和标记整理法,二者组合示意图以下,这二者的组合因为都是多线程收集器,真正实现了「吞吐量优先」的目标

CMS 收集器

CMS 收集器是以实现最短 STW 时间为目标的收集器,若是应用很重视服务的响应速度,但愿给用户最好的体验,则 CMS 收集器是个很不错的选择!

咱们以前说老年代主要用标记整理法,而 CMS 虽然工做于老年代,但采用的是标记清除法,主要有如下四个步骤

  1. 初始标记
  2. 并发标记
  3. 从新标记
  4. 并发清除

从图中能够的看到初始标记和从新标记两个阶段会发生 STW,形成用户线程挂起,不过初始标记仅标记 GC Roots 能关联的对象,速度很快,并发标记是进行 GC Roots  Tracing 的过程,从新标记是为了修正并发标记期间因用户线程继续运行而致使标记产生变更的那一部分对象的标记记录,这一阶段停顿时间通常比初始标记阶段稍长,但远比并发标记时间短

整个过程当中耗时最长的是并发标记和标记清理,不过这两个阶段用户线程均可工做,因此不影响应用的正常使用,因此整体上看,能够认为 CMS 收集器的内存回收过程是与用户线程一块儿并发执行的。

可是 CMS 收集器远达不到完美的程度,主要有如下三个缺点

  • CMS 收集器对 CPU 资源很是敏感  缘由也能够理解,好比原本我原本能够有 10 个用户线程处理请求,如今却要分出 3 个做为回收线程,吞吐量降低了30%,CMS 默认启动的回收线程数是 (CPU数量+3)/ 4, 若是 CPU 数量只有一两个,那吞吐量就直接降低 50%,显然是不可接受的
  • CMS 没法处理浮动垃圾(Floating Garbage),可能出现 「Concurrent Mode Failure」而致使另外一次 Full GC 的产生,因为在并发清理阶段用户线程还在运行,因此清理的同时新的垃圾也在不断出现,这部分垃圾只能在下一次 GC 时再清理掉(即浮云垃圾),同时在垃圾收集阶段用户线程也要继续运行,就须要预留足够多的空间要确保用户线程正常执行,这就意味着 CMS 收集器不能像其余收集器同样等老年代满了再使用,JDK 1.5 默认当老年代使用了68%空间后就会被激活,固然这个比例能够经过 -XX:CMSInitiatingOccupancyFraction 来设置,可是若是设置地过高很容易致使在 CMS 运行期间预留的内存没法知足程序要求,会致使 Concurrent Mode Failure 失败,这时会启用 Serial Old 收集器来从新进行老年代的收集,而咱们知道 Serial Old 收集器是单线程收集器,这样就会致使 STW 更长了。
  • CMS 采用的是标记清除法,上文咱们已经提到这种方法会产生大量的内存碎片,这样会给大内存分配带来很大的麻烦,若是没法找到足够大的连续空间来分配对象,将会触发 Full GC,这会影响应用的性能。固然咱们能够开启 -XX:+UseCMSCompactAtFullCollection(默认是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理会致使 STW,停顿时间会变长,还能够用另外一个参数 -XX:CMSFullGCsBeforeCompation 用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的。

G1(Garbage First) 收集器

G1 收集器是面向服务端的垃圾收集器,被称为驾驭一切的垃圾回收器,主要有如下几个特色

  • 像 CMS 收集器同样,能与应用程序线程并发执行。
  • 整理空闲空间更快。
  • 须要 GC 停顿时间更好预测。
  • 不会像 CMS 那样牺牲大量的吞吐性能。
  • 不须要更大的 Java Heap


与 CMS 相比,它在如下两个方面表现更出色
  1. 运做期间不会产生内存碎片,G1 从总体上看采用的是标记-整理法,局部(两个 Region)上看是基于复制算法实现的,两个算法都不会产生内存碎片,收集后提供规整的可用内存,这样有利于程序的长时间运行。
  2. 在 STW 上创建了 可预测的停顿时间模型,用户能够指按期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间之内。

为何G1能创建可预测的停顿模型呢,主要缘由在于 G1 对堆空间的分配与传统的垃圾收集器不一器,传统的内存分配就像咱们前文所述,是连续的,分红新生代,老年代,新生代又分 Eden,S0,S1,以下

而 G1 各代的存储地址不是连续的,每一代都使用了 n 个不连续的大小相同的 Region,每一个Region占有一块连续的虚拟内存地址,如图示

除了和传统的新老生代,幸存区的空间区别,Region还多了一个H,它表明Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象,这样超大对象就直接分配到了老年代,防止了反复拷贝移动。那么 G1 分配成这样有啥好处呢?

传统的收集器若是发生 Full GC 是对整个堆进行全区域的垃圾收集,而分配成各个 Region 的话,方便 G1 跟踪各个 Region 里垃圾堆积的价值大小(回收所得到的空间大小及回收所需经验值),这样根据价值大小维护一个优先列表,根据容许的收集时间,优先收集回收价值最大的 Region,也就避免了整个老年代的回收,也就减小了 STW 形成的停顿时间。同时因为只收集部分 Region,可就作到了 STW 时间的可控。

G1 收集器的工做步骤以下

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

能够看到总体过程与 CMS 收集器很是相似,筛选阶段会根据各个 Region 的回收价值和成本进行排序,根据用户指望的 GC 停顿时间来制定回收计划。

总结

本文简述了垃圾回收的原理与垃圾收集器的种类,相信你们对开头提的一些问题应该有了更深入的认识,在生产环境中咱们要根据不一样的场景来选择垃圾收集器组合,若是是运行在桌面环境处于 Client 模式的,则用 Serial + Serial Old 收集器绰绰有余,若是须要响应时间快,用户体验好的,则用 ParNew + CMS 的搭配模式,即便是号称是「驾驭一切」的 G1,也须要根据吞吐量等要求适当调整相应的 JVM 参数,没有最牛的技术,只有最合适的使用场景,切记!

理论有了,下一篇咱们会进入手动操做环节,咱们会一块儿来动手操做一些 demo,作一些实验,来验证咱们看到的一些现象,好比对象通常分配在新生代,什么状况下会直接到老年代,该怎么实验?发生了OOM,该用哪些工具调试呢?等等,敬请期待!


参考

堆外内存的回收机制分析 https://www.jianshu.com/p/35cf0f348275 

java调用本地方法--jni简介 https://blog.csdn.net/w1992wishes/article/details/80283403 

我们从头至尾说一次 Java 垃圾回收 https://mp.weixin.qq.com/s/pR7U1OTwsNSg5fRyWafucA 

深刻理解 Java 虚拟机 

Java Hotspot G1 GC的一些关键技术 https://tech.meituan.com/2016/09/23/g1.html


    

往期推荐

千万不要这样写代码!9种常见的OOM场景演示


腾讯推出高性能 RPC 开发框架


Java中竟有18种队列?45张图!安排


关注我,天天陪你进步一点点!



本文分享自微信公众号 - Java中文社群(javacn666)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索