Java中有许多垃圾收集器(Garbage Collector,GC)可供选择,每个收集器都有其独特的特性和适用场景。
上图展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。这篇文章会介绍上面几种垃圾收集器
这是一个新生代的垃圾收集器,他有下面几个特点
还有一个Serial Old收集器用于老年代收集,下面示意了Serial/Serial Old收 集器的运行过程。
总之,Serial垃圾回收器是Java虚拟机的一种简单的垃圾回收器,适用于一些资源受限的环境或用于简单的测试和学习。但对于需要高并发性能和低停顿时间的应用程序,通常不建议使用Serial收集器,而应考虑其他更适合多核CPU的垃圾回收器。
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew收 集器的工作过程如图3-8所示。
ParNew收集器就可以简单理解为用于新生代的多线程Serial收集器
Parallel Scavenge(并行清除)收集器是Java虚拟机的一种垃圾回收器,主要用于新生代的垃圾回收。它被设计成在多核CPU上并行执行垃圾回收操作,旨在提供高吞吐量的垃圾回收性能。
以下是关于Parallel Scavenge收集器的重要信息:
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
Serial Old收集器的工作过程如下
Parallel Old是Parallel Scavenge收集器的老年代版本,用于老年代的垃圾回收。它是Parallel Scavenge垃圾回收器的补充,旨在提供多线程的、并行执行的老年代垃圾回收,以提高垃圾回收性能。
Parallel Old收集器的工作过程如图3-10所示。Parallel Scavenge和Parallel Old一般配合使用,是一款“吞吐量优先”的收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。主要用于老年代的垃圾回收。它的主要特点是尽量减少应用程序停顿时间,特别适用于对低停顿时间要求较高的应用。
CMS的运行分为以下四个过程:
通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。
CMS也有一些缺点,如下
内存碎片问题:CMS是一款基于“标记-清除”算法实现的收集器,所以会产生很多的内存碎片,这样分配大对象时就可能出现还有足够空间但是无法找 到足够大的连续空间来分配当前对象,需要提前触发一次Full GC的情况
性能问题:由于在并发标记和并发清理阶段一直在运行,所以会占用一条线程。如果处理器只有4个处理器核心,那么在并发标记和并发清理阶段就会一直占用25%的处理器运算资源
无法处理浮动垃圾:CMS不能处理浮动垃圾(Floating Garbage),即在并发标记阶段之后生成的垃圾。这些垃圾对象可能需要等待下一次垃圾回收才能被清除。
G1是一种分代收集器,但它的工作方式与传统的分代收集器(新生代和老年代)有所不同。它将整个堆划分为多个区域,每个区域可以属于新生代、老年代,或是混合区。这种划分允许G1更灵活地管理内存。
G1将整个Java堆划分为多个相等大小的区域,每个区域可以是新生代区、老年代区或混合区。每个区域的大小通常在1MB到32MB之间,这些区域构成了G1的可控制的内存管理单元。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。
G1最主要的一个特点就是并不是一次回收所有垃圾,一次只回收一部分垃圾。G1的主要目标是按照垃圾的数量优先回收。G1会选择包含垃圾最多的区域进行回收,以尽量减小垃圾回收的停顿时间。并且我们可以指定垃圾回收的停顿时间,这时G1只会在规定时间内回收最有价值的Region(价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表)
下图是G1的内存布局
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收集器的运作步骤中并发和需要停顿的阶段如下图