【JVM】经典垃圾收集器

说明

Java中有许多垃圾收集器(Garbage Collector,GC)可供选择,每个收集器都有其独特的特性和适用场景。

image-20230909175821479

上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。这篇文章会介绍上面几种垃圾收集器

新生代收集器

Serial收集器

这是一个新生代的垃圾收集器,他有下面几个特点

  1. 单线程垃圾回收器:Serial收集器是单线程的,也就是说它只使用一个线程来执行垃圾回收操作。因此,它不适合多核心的处理器,不利于并行回收。
  2. 新生代收集:Serial收集器主要用于新生代的垃圾回收。在新生代中,它使用复制(Copying)算法,将存活的对象复制到一个干净的区域,然后清理不再被引用的对象。这个过程是单线程执行的。
  3. Stop-the-World暂停:Serial收集器在进行垃圾回收时需要停止应用程序的执行,这会导致一段时间的应用程序停顿,这段时间被称为"Stop-the-World"暂停。因为Serial是单线程的,所以这个停顿时间相对较长。
  4. 适用场景:由于Serial收集器是单线程的,因此它通常不适用于多核心CPU或需要低延迟的应用程序。但它在资源受限的环境下可能是一个合适的选择,例如移动设备或嵌入式系统。

还有一个Serial Old收集器用于老年代收集,下面示意了Serial/Serial Old收 集器的运行过程。

image-20230909181536247

总之,Serial垃圾回收器是Java虚拟机的一种简单的垃圾回收器,适用于一些资源受限的环境或用于简单的测试和学习。但对于需要高并发性能和低停顿时间的应用程序,通常不建议使用Serial收集器,而应考虑其他更适合多核CPU的垃圾回收器。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew收 集器的工作过程如图3-8所示。

image-20230909181907214

ParNew收集器就可以简单理解为用于新生代的多线程Serial收集器

Parallel Scavenge收集器

Parallel Scavenge(并行清除)收集器是Java虚拟机的一种垃圾回收器,主要用于新生代的垃圾回收。它被设计成在多核CPU上并行执行垃圾回收操作,旨在提供高吞吐量的垃圾回收性能。

以下是关于Parallel Scavenge收集器的重要信息:

  1. 并行垃圾回收:Parallel Scavenge收集器是一种多线程垃圾回收器,它允许多个线程并行执行新生代的垃圾回收操作。这样可以充分利用多核CPU的性能,提高回收性能。
  2. 新生代收集:Parallel Scavenge主要用于新生代的垃圾回收。新生代的对象通常生命周期较短,因此使用复制算法进行垃圾回收,同时通过多线程提高回收效率。
  3. 高吞吐量:Parallel Scavenge的主要目标是提供高吞吐量,即在一段时间内尽可能多地执行应用程序的工作。它通常不追求极低的停顿时间,而是优化吞吐量。
  4. 自适应调整:Parallel Scavenge收集器具有自适应调整功能,可以根据应用程序的性能需求动态调整垃圾回收的策略。例如,可以自动调整新生代和老年代的比例,以更好地满足性能需求。
  5. Stop-the-World暂停:虽然Parallel Scavenge支持并行垃圾回收,但在进行垃圾回收时仍然需要进行短暂的"Stop-the-World"停顿,以确保一致性和安全性。
  6. 适用场景:Parallel Scavenge适用于需要高吞吐量的应用程序,通常用于后台处理任务、数据分析等需要大量计算的场景。它不太适合强调低停顿时间的应用程序。

老年代收集器

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

Serial Old收集器的工作过程如下

image-20230909182650027

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,用于老年代的垃圾回收。它是Parallel Scavenge垃圾回收器的补充,旨在提供多线程的、并行执行的老年代垃圾回收,以提高垃圾回收性能。

Parallel Old收集器的工作过程如图3-10所示。Parallel Scavenge和Parallel Old一般配合使用,是一款“吞吐量优先”的收集器

image-20230909182954285

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。主要用于老年代的垃圾回收。它的主要特点是尽量减少应用程序停顿时间,特别适用于对低停顿时间要求较高的应用。

CMS的运行分为以下四个过程:

  1. 初始标记(CMS initial mark):需要”Stop The World”,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
  2. 并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  3. 重新标记(CMS remark):需要“Stop The World”,这里是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录【增量更新】
  4. 并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。

image-20230909184051239

CMS也有一些缺点,如下

  • 内存碎片问题:CMS是一款基于“标记-清除”算法实现的收集器,所以会产生很多的内存碎片,这样分配大对象时就可能出现还有足够空间但是无法找 到足够大的连续空间来分配当前对象,需要提前触发一次Full GC的情况

  • 性能问题:由于在并发标记和并发清理阶段一直在运行,所以会占用一条线程。如果处理器只有4个处理器核心,那么在并发标记和并发清理阶段就会一直占用25%的处理器运算资源

  • 无法处理浮动垃圾:CMS不能处理浮动垃圾(Floating Garbage),即在并发标记阶段之后生成的垃圾。这些垃圾对象可能需要等待下一次垃圾回收才能被清除。

Garbage First收集器

G1是一种分代收集器,但它的工作方式与传统的分代收集器(新生代和老年代)有所不同。它将整个堆划分为多个区域,每个区域可以属于新生代、老年代,或是混合区。这种划分允许G1更灵活地管理内存。

G1将整个Java堆划分为多个相等大小的区域,每个区域可以是新生代区、老年代区或混合区。每个区域的大小通常在1MB到32MB之间,这些区域构成了G1的可控制的内存管理单元。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。

G1最主要的一个特点就是并不是一次回收所有垃圾,一次只回收一部分垃圾。G1的主要目标是按照垃圾的数量优先回收。G1会选择包含垃圾最多的区域进行回收,以尽量减小垃圾回收的停顿时间。并且我们可以指定垃圾回收的停顿时间,这时G1只会在规定时间内回收最有价值的Region(价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表)

下图是G1的内存布局

image-20230909192429688

image-20230909192343025

需要解决的问题

G1将堆内存“化整为零”的思路,将内存划分为多个不同大小的Region,由此也出现了许多需要解决的问题

  • 跨Region引用对象如何解决?:我们知道解决跨代引用是使用记忆集避免全堆作为GC Roots扫描的,但在G1收集器上记忆集的应用其实要复杂很多,因为它的每个Region都维护有自己的记忆集。G1的记忆集在存储结构的本质上是一 种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃 圾收集器有着更高的内存占用负担。

  • 如何保证垃圾回收的准确性?:CMS收集器采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法来实现的。垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设 计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过 程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在 这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。

  • 怎样建立起可靠的停顿预测模型?:G1收集器的停顿 预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

运作过程

如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的运作过程大致可划分为以下四个步骤:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。 (STW)

  • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。

  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。 (STW)

  • 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。(STW)

G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的。G1收集器的运作步骤中并发和需要停顿的阶段如下图

image-20230916164126393

CMS和G1的区别

  • CMS使用“标记-清除”算法。G1从整体来看是基于“标记-整理”,局部使用“标记-复制”算法。
  • CMS一次回收所有垃圾,吞吐量相对较低。G1一次回收部分垃圾,单次停顿时间可控,吞吐量更高。
  • CMS基于分代进行垃圾回收。G1基于Region进行垃圾回收。
  • CMS采用增量更新。G1使用原始快照。
  • CMS卡表简单,只有一份老年代到新生代的引用。G1卡表复杂,每个Region都要有一份,导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间。
  • CMS用写后屏障来更新维护卡表。G1使用写前屏障和邂逅屏障来维护卡表。
  • 在小内存应用上CMS的表现大概率仍然要会优于G1。大内存应用上G1则大多能发挥其 优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。
  • G1基本已经取代CMS,CMS已经被官方标记为不推荐使用了。
    维护卡表。G1使用写前屏障和邂逅屏障来维护卡表。
  • 在小内存应用上CMS的表现大概率仍然要会优于G1。大内存应用上G1则大多能发挥其 优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。
  • G1基本已经取代CMS,CMS已经被官方标记为不推荐使用了。

猜你喜欢

转载自blog.csdn.net/m0_51545690/article/details/132921293