avatar

27.JVM之垃圾回收算法及常用垃圾回收器介绍

0x00 前言

经过上篇文章的阅读,相信我们对JVM有个最基本的了解。我们知道Java和C++最大的区别就是垃圾回收。
打个比喻说,同样是去餐厅吃饭,对于Java来说,你吃完后,根本不需要关心你扔在桌上的餐具,会有一个专门的“服务员”来替你回收他们到厨房;而C++就不同了,你吃完后,得自己收拾桌子,自己送到厨房。那么Java是通过什么算法来确定该对象是垃圾的呢?以及是通过什么样的算法去回收呢?下面就让我们一起学习下吧。

0x01 JVM是如何确定“垃圾”的?

  1. 首先我们需要明确的一点是:JVM垃圾回收主要关注在哪部分。根据上篇JVM内存结构介绍,我们对JVM内存结构有了一定的认识,至少知道了堆、虚拟机栈、本地方法栈、方法区、程序计数器这些名词。那么JVM重点关注这5个区域中的哪个呢?没错,重点关注的对象是“堆”。为什么呢?因为虚拟机栈、本地方法栈、程序计数器是随着线程开始而生,随着线程结束而灭,所以是不会有垃圾产生的。至于方法区,存放的是一些常量、类定义等东西,也是有gc产生,但是不会那么频繁。
  2. 既然我们知道垃圾回收重点关注的是堆空间,那么我们先来谈谈堆内空间的分布吧。
    1. 在Java中堆又被划分为2个区域,即常说的新生代(Young)和老年代(Old)。内存模型大致如下:
      • 新生代
        • 新生代又被分为3个区域,即:Eden区、From Survivor(from/s0)、To Survivor(to/s1).其中From Survivor和To Survivor又被称为from区和to区,有时也叫s0区和s1区。Eden区和form区以及to区,他们的空间占比是8:1:1,一般来说from区和to区的空间是一样的,为什么会这样,在下面的垃圾回收算法中会提到。
        • 每个对象一出生(被new出来后),绝大部分第一站就是新生代的Eden区,这里引入一个年龄的概念,刚出生时,对象的年龄为1,每次经过一次垃圾回收后,如果还存活,年龄就会+1。当经历过第一次垃圾收集后,如果对象还存活,则会进入到s0或s1区,之后经过多次垃圾回收后,该对象还在,即年龄越来越大,则会把对象放入老年代中。
      • 老年代
        • 存放频繁使用的对象。老年代与新生代的比例是2:1,当然,这个比例是可以配置的,具体方式我们下次再聊。
    2. 如何判断一个对象是否存活?
      1. 引用计数法
        • 引用计数法,顾名思义就是如果一个对象没有任何一个引用指向,则就看做成是垃圾,即给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。
        • 该方法缺点很明显,碰到循环引用就没得办法了。所以JVM没有使用引用计数法来确定对象是不是垃圾。
      2. 根搜索算法
        • 根搜索算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。也就是说每次搜索的时候都是从“GC Roots”往下搜索,如果对象与“GC Roots”有链接,则说明对象可用,反之则说明对象不可用。如图:

        • 那么GC Root对象是怎么选取的呢?不能随便一个对象都叫做GC Root吧。在Java中,可作为GC Root对象的有如下几个:

          • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中引用的对象
          • 方法区中的类静态属性引用的对象。
          • 方法区中常量引用的对象。
          • 本地方法栈中JNI(Native方法)引用的对象。

0x02 JVM垃圾回收算法介绍

  1. 经过上面的一些介绍,我们至少可以懂得,垃圾回收主要作用在“堆”中,然后也知道了JVM是通过根搜索算法来判断对象是否存活。那么下面就让我们来看看JVM是怎么进行垃圾回收的吧。

  2. 标记清除算法

    1. 概念:
      • 既然是标记清除算法,那么该算法就会有两个阶段。
        • 标记:采用根搜索算法找到所有可访问的对象,这些对象就是有用的对象。
        • 清除:遍历堆,把未标记的对象清除,即清除垃圾。
    2. 图解:
    3. 优缺点
      • 优点
        • 是可以解决循环引用的问题
        • 必要时才回收(内存不足时)
      • 缺点
        • 回收时,应用需要挂起,也就是stop the world。
        • 标记和清除的效率不高,尤其是要扫描的对象比较多的时候
        • 会造成内存碎片(会导致明明有内存空间,但是由于不连续,申请稍微大一些的对象无法做到)
    4. 应用场景
      • 该算法一般应用于老年代,因为老年代的对象生命周期比较长,回收不需要太频繁.
  3. 复制算法

    1. 概念
      • 上面介绍“堆内空间划分”的时候,说到了在堆中分为新生代和老年代,而新生代中又分Eden、from、to区,而且from、to区大小是一样的,并且每次只会使用一个区域。那么为什么要划分from、to区呢?答案就是为了更加高效的执行复制算法。
      • 复制算法,简单来说当from区域内存不够了,开始执行GC操作,这个时候,会把from区域存活的对象拷贝到to区域,然后直接把from域进行内存清理,然后当to区内存不够了,又会把to区中还存活的对象拷贝到from区,然后清空to区,如此反复。
    2. 图解
    3. 优缺点
      1. 优点:
        • 在存活对象不多的情况下,性能高,能解决内存碎片和java垃圾回收算法之-标记清除中导致的引用更新问题。
      2. 缺点:
        • 会造成一部分的内存浪费。不过可以根据实际情况,将内存块大小比例适当调整;如果存活对象的数量比较大,复制算法的性能会变得很差。
    4. 应用场景
      • 一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用复制算法进行拷贝时效率比较高。
      • jvm将Heap 内存划分为新生代与老年代,又将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区) ,然后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。 不过jvm在应用coping算法时,并不是把内存按照1:1来划分的,这样太浪费内存空间了。一般的jvm都是8:1。也即是说,Eden区:From区:To区域的比例是8:1:1,即始终有90%的空间是可以用来创建对象的,而剩下的10%用来存放回收后存活的对象。
    5. 新生代使用复制算法垃圾清理的过程:
      1. 对象new出来的时候会放在Eden区,当Eden区满的时候,会触发一次young gc,这时会把Eden区中还存活的对象(被GC Roots引用的对象)放到from区。
      2. 当Eden区再次满的情况下,再一次触发young gc,这时候会扫描Eden区和from区,把还存活的对象,放到to区中,然后清空Eden区和from区。
      3. 当后续Eden又发生young gc的时候,会对Eden和To区域进行垃圾回收,存活的对象复制到From区域,并将Eden和To区域清空。
      4. 可见部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代.
      • 注意: 万一存活对象数量比较多,那么To域的内存可能不够存放,这个时候会借助老年代的空间。
  4. 标记压缩算法

    1. 概念
      • 标记压缩算法和标记删除算法非常相同,只是在标记删除算法上多加了一个解决内存碎片化的处理。
    2. 图解
    3. 优缺点
      1. 优点
        • 解决内存碎片问题,缺点压缩阶段,由于移动了可用对象,需要去更新引用。
      2. 缺点
        • 和标记删除算法一样,回收时,应用需要挂起,也就是stop the world。
    4. 应用场景
      • 该算法一般用于老年代
  5. 分代算法

    1. 概念
      • 根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。可以用抓重点的思路来理解这个算法。
      • 新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率。另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销。
      • 简单来说,就是新生代和老年代根据特点采用不同的算法,加起来就是分代算法。
    2. 新生代一般采用的垃圾收集算法:
      • 在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,所以采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集;
    3. 老年代一般采用的垃圾收集算法:
      • 老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须“标记-清除-压缩”算法进行回收。

0x03 常用的垃圾收集器介绍

  1. 什么是串行,什么又是并行?
    • 串行
      • 通过一个线程去做垃圾回收,执行垃圾回收时程序暂停的时间比较长。
    • 并行
      • 使用多个线程去做垃圾回收,提高垃圾回收效率,回收时系统会暂停运行。
  2. serial收集器
    • 串行收集器,是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,因为期只使用一个线程去回收。
    • 该收集器新生代、老年代都使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)。
    • 特点:
      • CPU利用率最高,停顿时间即用户等待时间比较长。
    • 通过JVM参数-XX:+UseSerialGC可以使用串行垃圾回收器。
  3. ParNew收集器
    • ParNew收集器其实就是Serial收集器的多线程版本
    • 该收集器新生代使用并行回收,老年代使用串行回收;新生代复制算法、老年代标记-压缩算法。
    • 特点:
      • 停顿时间短,回收效率高,适合对吞吐量要求高的应用。
    • 通过JVM参数 XX:+USeParNewGC 打开并发标记扫描垃圾回收器。
  4. cms收集器
    • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
    • CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
      • 初始标记(CMS initial mark)
        • 仅仅做标记一下GC Roots能直接关联到的对象的操作,速度很快。
        • 会“Stop The World”,即暂停系统
      • 并发标记(CMS concurrent mark)
        • 并发标记阶段就是进行GC Roots Tracing的过程
      • 重新标记(CMS remark)
        • 重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
        • 会“Stop The World”,即暂停系统
      • 并发清除(CMS concurrent sweep)
    • 特点:
      • 响应时间优先,减少垃圾收集停顿时间。
    • 通过JVM参数 -XX:+UseConcMarkSweepGC设置
  5. g1收集器
    • 在G1中,堆被划分成 许多个连续的区域(region)。采用G1算法进行回收,吸收了CMS收集器特点。
    • 特点:
      • 支持很大的堆,高吞吐量
    • 通过JVM参数 -XX:+UseG1GC 使用G1垃圾回收器

0x4 并行和并发的区别

  1. 并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
  2. 并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
  3. 举个简单的例子:并发就是一条道上“同时”跑3辆车,而并行是三条道上“同时”跑3辆车。

0x05 Minor GC、Major GC和Full GC简单介绍

  1. Minor GC
    • 从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。也就是说,发生在年轻代中的gc回收,就叫minor GC。因为其回收的年轻代空间,而对象又有朝生夕死的特性,所以该Minor GC次数很频繁,因为采用的是复制算法,所以一般其回收速度很快。
    • 什么时候触发?
      • 当年轻代满时就会触发Minor GC,这里的年轻代满指的是Eden代满,Survivor满不会引发GCFull GC触发机制;
  2. Major GC
    • 发生在老年代的GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的),MajorGC 的速度一般会比 Minor GC慢10倍以上。
  3. Full GC
    • 是清理整个堆空间(包括年轻代和老年代以及永久代(Java8中被元数据区取代))。
    • 什么时候触发?
      • 当年老代满时会引发Full GC,Full GC将会同时回收年轻代、年老代。
      • 当永久代满时也会引发Full GC,会导致Class、Method元信息的卸载。
      • 当显示调用System.gc()时

0x06 总结

  1. 这篇介绍的内容都是和JVM垃圾收集相关的,这里来整理下吧。
    • 垃圾回收算法有如下几个:
      • 标记清除
      • 复制算法
      • 标记压缩
      • 分代算法
    • 常用的垃圾收集器有如下几个:
      • serial收集器(串行回收器)
      • ParNew收集器
      • cms收集器
      • g1收集器
    • 并发和并行的区别
      • 并发就是一条道上“同时”跑3辆车,而并行是三条道上“同时”跑3辆车。
    • Minor GC、Major GC和Full GC
      • Minor GC:年轻代中发生GC,统称为Minor GC。
      • Major GC:老年代中发生GC,统称为Major GC。
      • Full GC:清理整个堆空间
  2. 参考文章及书籍:
    1. 《深入理解Java虚拟机——JVM高级特性与最佳实践》周志明
    2. JVM的垃圾回收机制 总结(垃圾收集、回收算法、垃圾回收器)

评论