JVM 之 垃圾收集器

前言

前面我们学习了GC的算法,本节我们将学习GC 的垃圾收集器。重点学习G1收集器


概要

在虚拟机规范中并没有对垃圾回收器如何实现具体介绍,因此每个厂商的垃圾回收器可能会完全不同,但是我们介绍的是基于JDK1.7之后的Hotspot虚拟机(包括前面对Java虚拟机的介绍也是基于jdk1.7版本的)。在Hotspot中,虚拟机的收集器主要有下:
这里写图片描述
可以看到垃圾收集器是按对象的分代来划分的,可以用线条连接的垃圾回收器表示两者可以配合使用。

  • 新生代
    • Serial
    • ParNew
    • Parallel Scavenge
    • G1
  • 老年代
    • CMS
    • Parallel Old
    • Serial Old
    • G1

G1是一种既可以对新生代对象也可以对老年代对象进行回收的垃圾收集器。然而,在所有的垃圾收集器中,并没有一种普遍使用的垃圾收集器。在不同的场景下,每种垃圾收集器有各自的优势。


Serial收集器

Serial是最基本也是发展最悠久的收集器。它是一种单线程垃圾收集器,这就意味着在其进行垃圾收集的时候需要暂停其他的线程,也就是之前提到的”Stop the world“。虽然这个过程是在用户不可见的情况下把用户正常的线程全部停掉,听起来有点狠,这点是很难让人接受的。Serial收集器的工作示意图如下:
这里写图片描述
尽管由以上不能让人接受的地方,但是Serial收集器还是有其优点的:简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得较高的手机效率。此外,到目前为止,Serial收集器依然是Client模式下的默认的新生代垃圾收集器。


ParNew收集器

可以把这个收集器理解为Serial收集器的多线程版本,ParNew收集器的工作示意图如下:
这里写图片描述
ParNew收集器是许多运行在Server模式下的默认新生代垃圾收集器,为什么不选用Serial作为新生代的收集器呢?主要在于除了Serial收集器,目前只有ParNew收集器能够与CMS收集器配合工作。


Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代垃圾收集器,其使用的算法是复制算法,也是并行的多线程收集器(所谓”并行”,就是指多条垃圾收集线程同时工作,但是用户仍处于等待状态;有一个相似的概念“并发”,指的是用户线程与垃圾收集线程同时执行)。这点ParNew与Parallel Scavenge收集器是一样的,那么Parallel Scavenge收集器有什么新特性呢?

区别在于Parallel Scavenge收集器更关注可控制的吞吐量,吞吐量等于运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。

这个参数有什么意义呢?根据数据知识,吞吐量越大,意味着垃圾收集的时间越短,则用户代码则可以充分利用CPU资源,尽快完成程序的运算任务。

Parallel Scavenge收集器使用两个参数控制吞吐量:-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,-XX:GCRatio直接设置吞吐量的大小。

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。

除此之外,Parallel Scavenge收集器还可以设置参数-XX:+UseAdaptiveSizePocily来动态调整停顿时间或者最大的吞吐量,这种方式称为GC自适应调节策略,这点是ParNew收集器所没有的。

扫描二维码关注公众号,回复: 1690077 查看本文章

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与Serial收集器一样。


Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。这个组合的工作过程如下:
这里写图片描述


CMS收集器

CMS收集器(Concurrent Mark Sweep)的目标就是获取最短回收停顿时间。在注重服务器的响应速度,希望停顿时间最短,则CMS收集器是比较好的选择。整个执行过程分为以下4个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

初始标记和重新标记这两个步骤仍然需要暂停Java执行线程,初始标记只是标记GC Roots能够关联到的对象,并发标记就是执行GC Roots Tracing的过程,而重新标记就是为了修正并发标记期间因用户程序执行而导致标记发生变动使得标记错误的记录。其执行过程如下:
这里写图片描述

  • 优点:
    • 并发收集、低停顿(由于进行垃圾收集的时间主要耗在并发标记与并发清除这两个过程,虽然初始标记和重新标记仍然需要暂停用户线程,但是从总体上看,这部分占用的时间相比其他两个步骤很小,所以可以认为是低停顿的)。
  • 缺点:
    • 对CPU资源太敏感,这点可以这么理解,虽然在并发标记阶段用户线程没有暂停,但是由于收集器占用了一部分CPU资源,导致程序的响应速度变慢
    • CMS收集器无法处理浮动垃圾。所谓的“浮动垃圾”,就是在并发标记阶段,由于用户程序在运行,那么自然就会有新的垃圾产生,这部分垃圾被标记过后,CMS无法在当次集中处理它们(为什么?原因在于CMS是以获取最短停顿时间为目标的,自然不可能在一次垃圾处理过程中花费太多时间),只好在下一次GC的时候处理。这部分未处理的垃圾就称为“浮动垃圾”
    • 由于CMS收集器是基于“标记-清除”算法的,前面说过这个算法会导致大量的空间碎片的产生,一旦空间碎片过多,大对象就没办法给其分配内存,那么即使内存还有剩余空间容纳这个大对象,但是却没有连续的足够大的空间放下这个对象,所以虚拟机就会触发一次Full GC(这个后面还会提到)这个问题的解决是通过控制参数-XX:+UseCMSCompactAtFullCollection,用于在CMS垃圾收集器顶不住要进行FullGC的时候开启空间碎片的合并整理过程。

G1收集器

G1(Garbage-First)收集器是现今收集器技术的最新成果之一,之前一直处于实验阶段,直到jdk7u4之后,才正式作为商用的收集器。与前几个收集器相比,G1收集器有以下特点:

  • 并行与并发
  • 分代收集(仍然保留了分代的概念)
  • 空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
  • 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)

此外,G1收集器将Java堆划分为多个大小相等的Region(独立区域),新生代与老年代都是一部分Region的集合,G1的收集范围则是这一个个Region:
这里写图片描述
每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率。

  • 新生代收集
    堆是分割成许多固定大小区域的一个存储区域。区域大小由JVM在启动时选择。JVM通常针对2000个区域,大小从1到32Mb不等。

    • G1堆结构
      堆是分割成许多固定大小区域的一个存储区域。
      这里写图片描述

    • G1堆分配
      这里写图片描述
      图片中的颜色显示哪个区域与哪个角色相关联。活物体从一个区域撤离(即复制或移动)到另一个区域。区域被设计为在停止或不停止所有其他应用程序线程的情况下并行收集。如图所示,地区可分配到Eden,Survivor和Tenured。此外,还有第四种称为Humongous地区的物体。这些区域设计用于容纳标准区域或更大尺寸的50%的物体。它们被存储为一组连续的区域。最后,最后一类区域将是堆中未使用的区域。

      注意:收集巨大的物体还没有被优化。因此,您应该避免创建此大小的对象。

    • G1中的新生代
      堆被分成大约2000个区域。最小尺寸为1Mb,最大尺寸为32Mb。蓝色区域表示Tenured(老年代),绿色区域表示Eden/Survivor。
      这里写图片描述
    • G1中的新生代GC

      • 标记
        有引用的对象(Alive-Object)撤离(即复制或移动)到一个或多个幸存区域。如果满足老化阈值,则将一些对象提升到老一代地区
        这里写图片描述
        这是一个阻止世界(STW)的停顿。计算下一个年轻的GC的伊甸园规模和幸存者规模。会计信息保存以帮助计算大小。诸如暂停时间目标等事情都会被考虑在内。

        这种方法可以轻松调整区域的大小,并根据需要调整区域的大小。

      • 清除
        有引用的对象(Alive-Object)已经撤离到幸存地区或老一代地区。
        这里写图片描述
        最近提升的对象以深蓝色显示。绿色的Survivor Area。
    • 小结

      • 堆是分割成区域的单个内存空间。
      • 年轻一代记忆由一组不连续的区域组成。这可以在需要时轻松调整大小。
      • 年轻一代的垃圾收集,或年轻的GC,是停止世界事件。所有应用程序线程都停止运行。
      • 年轻的GC是使用多线程并行完成的。
      • 活动对象被复制到新的幸存者或老一代地区。
  • 老年代收集
    G1收集器在老年代堆上执行以下阶段。请注意,有些阶段是新生代收藏的一部分。
    这里写图片描述

    • 初始标记阶段
      Alive-Object 的初始标记搭载在新生代垃圾收集器上。在日志中这被记录为GC pause (young)(inital-mark)。
      这里写图片描述
    • 并发标记阶段
      如果找到空白区域(如“X”所示),则会在备注阶段立即删除它们。此外,计算确定活跃度的“会计”信息。
      这里写图片描述
    • 备注阶段
      空白区域被删除并回收。现在计算所有地区的地区活跃度。
      这里写图片描述
    • 复制/清理阶段
      G1选择具有最低“活性”的区域,这些区域是可以最快采集的区域。然后这些区域与年轻的GC同时被收集。这在日志中表示为[GC pause (mixed)]。所以新生代和老年代都是同时收集的。
      这里写图片描述
    • 复制/清理阶段后
      所选区域已被收集并压缩成图中所示的深蓝色区域和深绿色区域。
      这里写图片描述

    • 小结

      • 并发标记阶段

        • 活动信息在应用程序运行时同时计算。
        • 这种活跃性信息确定撤离暂停期间哪些区域最适合回收。
        • 在CMS中没有扫描阶段。
      • 备注阶段

        • 使用Snapshot-at-the-Beginning(SATB)算法,这比使用CMS的速度快得多。
        • 完全空白的区域被回收。
      • 复制/清理阶段

        • 年轻一代和老一代同时被收回。
        • 老一代地区是根据他们的活跃度选择的。

G1收集器中, Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用都是使用Remembered Set来避免扫描全堆. G1中每个Region都有一个与之对应的Remembered Set, VM发现程序对Reference类型数据进行写操作时, 会产生一个Write Barrier暂时中断写操作, 检查Reference引用的对象是否处于不同的Region中(在分代例子中就是检查是否老年代中的对象引用了新生代的对象), 如果是, 便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中. 当内存回收时, 在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏.

猜你喜欢

转载自blog.csdn.net/u012437781/article/details/80182118