微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

JVM--垃圾回收器

目录

一、摘要

垃圾收集器有很多,不同商家、不同版本的J VM 所提供的垃圾收集器可能会有很在差别,我们主要介绍 HotSpot 虚拟机中的垃圾收集器。

垃圾收集器是垃圾回收算法的具体实现,我们上面提到过,垃圾回收算法有标记-清除算法、标记-整理、标记-复制,所以对应的垃圾收集器也有不同的实现方式。

HotSpot 虚拟机中的垃圾收集都是分代回收的,所以根据不同的分代,可以把垃圾收集器分为:

「新生代收集器」:Serial、ParNew、Parallel Scavenge;
「老年代收集器」:Serial Old、Parallel Old、CMS;
「整堆收集器」:G1;

二、Serial 收集器

Serial 收集器是最基础、历史最悠久的收集器,曾经是 HotSpot 虚拟机新生代收集器的唯一选择。

这个收集器是一个「单线程」工作的收集器,但它的单线程的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是「强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束」。 也就是说它在进行垃圾收集时,会发生「Stop The World」。

Serial 收集器的示意图如下:

在这里插入图片描述

「SefePoint 安全点」:它就是代码中的一段特殊的位置,在所有用户线程到达 SafePoint 之后,「用户线程挂起,GC 线程会进行清理工作」。

看起来 Serial 收集器已经是老而无用了,但事实上它依然是 HotSpot 虚拟机运行在客户端模式下的认新生代收集器。

对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的。对于单核处理器或处理器核心数较少的环境来说, Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

关键词:新生代、单线程、标记复制。

三、ParNew 收集器

ParNew 收集器实质上是 「Serial 收集器的多线程并行版本」,除了同时使用「多条线程」进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法、 Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一致。

ParNew 收集器的示意图如下:

在这里插入图片描述

虽然 ParNew 使用了多条线程进行垃圾回收,但是在单线程环境下它绝对不会比 Serial 收集效率更高,因为多线程存在线程交互的开销,但是随着可用 cpu 核数的增加,ParNew 的处理效率会比 Serial 更高效。
关键词:新生代、多线程、标记复制。

四、Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一款新生代收集器,它同样是「基于标记-复制」算法实现的收集器,也是能够并行收集的「多线程」收集器。它的关注点与其他收集器不同, Parallel Scavenge 的关注点主要在达到一个可控制的吞吐量上面。吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比。

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)

这里给大家举一个吞吐量的例子,如果执行用户代码的时间 + 运行垃圾收集的时间总共耗费了 100 分钟,其中垃圾收集耗费掉了 1 分钟,那么吞吐量就是 99%。

停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验。而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务, 主要适合在后台运算而不需要太多交互的分析任务。

Parallel Scavenge 收集器还有一个参数,当这个参数被激活之后,就不需要人工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象大小等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

这种调节方式称为垃圾收集的「自适应的调节策略」,这种策略也是 Parallel Scavenge 收集器区别于 ParNew 收集器的一个重要特性。
关键词:新生代、多线程、标记复制、 吞吐量、自适应调节。

五、Serial Old 收集器

Serial Old 是Serial 收集器的「老年代版本」,它同样是一个「单线程」收集器,使用「标记-整理」算法,这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。

在这里插入图片描述

关键词:老年代、单线程、标记整理。

六、Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的「老年代版本」,支持多线程并发收集,基于「标记-整理」算法实现。直到 Parallel Old 收集器出现后,“吞吐量优先” 收集器终于有了比较名副其实的搭配组合, 在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器这个组合。

Parallel Scavenge / Parallel Old 收集器运行示意图:

在这里插入图片描述

关键词:老年代、 多线程、标记整理。

七、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以「获取最短回收停顿时间」为目标的收集器,从名字上就可以看出 CMS 收集器是基于「标记-清除」算法实现的。

它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括: 「初始标记、并发标记、重新标记、并发清除」。 其中「初始标记、重新标记」这两个步骤仍然需要「Stop The World」。

CMS 的收集过程如下:

在这里插入图片描述

  1. 初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快。
  2. 并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  3. 重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录, 这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
  4. 并发清理阶段,清理删除标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿,一些官方
公开文档里面也称之为「并发低停顿收集器」。 CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:

  1. 并发阶段,虽然不会导致用户线程停顿,却因为占用一部分线程而导致应用程序变慢,「降低总吞吐量」。
  2. 它无法处理「浮动垃圾」,有可能会出现并发失败进而导致另一次Full GC的发生。
  3. 它是一款基于「标记清除」算法实现的收集器,这意味着收集结束时会有大量「空间碎片」产生。

什么是浮动垃圾
在CMS的「并发标记和并发清理」阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后, CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。 这一部分垃圾就称为“浮动垃圾”。

什么是并发失败?
由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。

到了JDK 6时,CMS收集器的启动阈值就已经提升至92%。但这更容易面临另一种风险: 要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次「并发失败」, 这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

关键词:老年代、低停顿、标记清除、 3标记1清除。

八、G1 收集器 (Garbage First)

Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,为什么说它是里程碑呢?因为 G1 这个收集器是一种面向局部的垃圾收集器,HotSpot 团队开发这个垃圾收集器为了让它替换掉 CMS 收集器,所以到后来, 直到 JDK 9 发布之日, G1宣告取代 Parallel Scavenge 加 Parallel Old 组合,成为服务端模式下的垃圾收集器,而 CMS 则沦落至被声明为不推荐使用的收集器。

在 G1 收集器出现之前的所有其他收集器,包括 CMS 在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个 Java堆(Full GC)。而G1跳出了这个限制,它可以面向堆内存任何部分来组成「回收集」进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的「Mixed GC模式」。

G1也仍是「遵循分代收集理论」设计的,但其堆内存的布局与其他收集器有非常明显的差异: G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的「独立区域(Region)」,每一个Region都可以根据需要,扮演新生代的 Eden空间、 Survivor空间,或者老年代空间。

此外, 还有一类专门用来存储大对象的特殊区域(Humongous Region)。它是专门用来存储大对象的,G1 认为只要大小超过了 Region 容量一半的对象即可判定为大对象。如果超过了 Region 容量的大对象,将会存储在连续的 Humongous Region 中,G1 大多数行为都会吧 Humongous Region 作为老年代来看待。

G1 保留了新生代(Eden Suvivor)和老年代的概念,但是新生代和老年代不再是固定的了。它们都是一系列区域的动态集合。

更具体的处理思路是,让G1收集器去跟踪各个Region里面的垃圾堆积的「价值」大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(认是200毫秒), 优先处理回收价值收益最大的那些Region,这也就是「Garbage First」名字的由来。

G1 收集器的运作过程可以分为以下四步:

  1. 「初始标记」:这个步骤也仅仅是标记一下 GC Roots 能够直接关联到的对象;并修改 TAMS 指针的值(每一个 Region 都有两个 RAMS 指针),让下一阶段用户并发运行时,能够在可用的 Region 中分配对象,这个阶段需要暂停用户线程,但是时间很短。这个停顿是借用 Minor GC 的时候完成的,耗时很短。
  2. 「并发标记」:从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆中的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成后,重新处理 SATB 记录下的在并发时有引用的对象;
  3. 「最终标记」:对用户线程做一个短暂的暂停,用于处理并发阶段结束后遗留下来的少量 SATB 记录(一种原始快照,用来记录并发标记中某些对象)
  4. 「筛选回收」:负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择多个 Region 构成回收集,然后把决定要回收的那一部分 Region 存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作设计对象的移动,所以必须要暂停用户线程,由多条收集器线程并行收集。

从上面这几个步骤可以看出,除了并发标记外,其余三个阶段都需要暂停用户线程,所以,这个 G1 收集器并非追求低延迟,官方给出的设计目标是在延迟可控的情况下尽可能的提高吞吐量,担任全功能收集器的重任。

下面是 G1 回收的示意图:

在这里插入图片描述


G1 收集器同样也有缺点和问题:

1、将Java堆分成多个独立Region后, Region里面存在的跨Region引用对象如何解决

G1 收集器使用 记忆集 避免全堆作为 GC Roots 扫描。但在 G1 收集器上记忆集的实现其实要复杂很多。
它的每个 Region 都维护有自己的记忆集, 这些记忆集会记录下别的 Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。

G1 的记忆集本质上是一种哈希表, Key是别的 Region 的起始地址, Value 是一个集合,里面存储的元素是表的索引号。这种双向的卡表结构比原来的卡表实现起来更复杂,同时由于 Region 数量比传收集器的分代数量明显要多得多,因此 G1 收集器要比其他的传统垃圾收集器有着更高的内存占用负担。根据经验, G1 至少要耗费大约相当于 Java 堆容量 10% 至 20% 的额外内存来维持收集器工作。

2、在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?

首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,导致标记
结果出现错误。针对该问题的解决方案, CMS收集器采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法来实现的。

此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上。 G1为每一个 Region 设计了两个名为 TAMS(Top at Mark Start)的指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。 G1 收集器在这个地址以上的对象是被隐式标记过的,即认它们是存活的,不纳入回收范围。

3、怎样建立起可靠的停顿预测模型?

G1 收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过
程中, G1 收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息

这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状
态,但衰减平均值更准确地代表“最近的”平均状态。换句话说, Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

关键词: 整堆、 Region、标记清理、 3标记1回收。

九、G1与CMS

G1与CMS的对比:

G1 从整体来看是基于标记整理算法实现的收集器,但从局部上看又是基于标记复制算法实现。 无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片垃圾收集完成之后能提供规整的可
用内存。

比起CMS,G1的弱项也可以列举出不少。例如在用户程序运行过程中, G1无论是为了垃圾收集产
生的内存占用还是程序运行时的额外执行负载都要比 CMS 要高。

G1与CMS的选择:

目前在小内存应用上 CMS 的表现大概率仍然要会优于 G1,而在大内存应用上 G1 则大多能发挥其优势,这个优劣势的 Java 堆容量平衡点通常在 6GB 至 8GB 之间。 以上这些也仅是经验之谈,随着HotSpot的开发者对G1的不断优化,也会让对比结果继续向G1倾斜。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐