JVM 垃圾回收器 详解
垃圾收集器
Serial+Serial Old
:单线程回收,适用于单核CPU场景ParNew+CMS
:暂停时间较短,适用于大型互联网应用中与用户交互的部分Paraller Scavenge+Parallel Old
:吞吐量高,适用于后台进行大量数据操作G1
:适用于较大堆,具有可控暂停时间
七种经典垃圾回收器
Serial 收集器:
最基础、历史最悠久的收集器,是一个单线程工作的收集器,工作于新生代,使用标记-复制算法:
简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的;对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择
ParNew 收集器:
ParNew收集器实质上是Serial收集器的多线程并行版本,ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,工作于新生代,使用标记-复制算法
Parallel Scavenge 收集器:
Parallel Scavenge 工作于新生代,使用标记-复制算法,也是能够并行收集的多线程收集器。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值,即:
Parallel Scavenge 收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio 参数。
- -XX:MaxGCPauseMillis 参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。
- -XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
Serial Old 收集器:
Serial Old 是 Serial 收集器的老年代版本,使用标记-整理算法,它同样是一个单线程收集器。这个收集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用(效果不是很好),另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old 收集器:
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,基于标记-整理算法实现,支持多线程并发收集。主要解决了Parallel Scavenge的老年代垃圾收集器的搭配问题。这个收集器是直到JDK 6时才开始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,而老年代的Serial Old收集器在服务端表现并不良好,这种组合的总吞吐量不一定比ParNew加CMS来得优秀,而Parallel Scavenge主打的就是吞吐量可控制
直到Parallel Old 收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge 加 Parallel Old 收集器这个组合。
CMS (Concurrent Mark Sweep)收集器:
JDK1.5时引入,JDK9被标记弃用,JDK14被移除
主打获取最短回收停顿时间(Stop The World),工作于老年代,基于标记-清除算法(混合标记-整理),整个过程分为四个步骤:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记需要标记GC Roots(包括年轻代存活下来的对象,因为需要解决跨代引用问题)能直接关联到的对象;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间, 因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(三色标记中的增量更新解决方案),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
CMS(Concurrent Mark-Sweep)垃圾收集器的缺点
- 对处理器资源敏感:
- CMS 在并发阶段会占用一部分处理器资源,导致应用程序变慢,降低总吞吐量。
- 默认启动的回收线程数是(处理器核心数量 + 3)/ 4,当处理器核心数量较少时,CMS 对用户程序的影响可能较大。
- 浮动垃圾问题:
- 在 CMS 的并发标记和并发清理阶段,用户线程继续运行,会产生新的垃圾对象(浮动垃圾)。
- 这些浮动垃圾无法在当次垃圾收集中处理,只能留待下一次收集。
- 浮动垃圾可能导致 “Concurrent Mode Failure”,进而触发完全 “Stop The World” 的 Full GC。
- 内存预留与并发失败风险:
- CMS 需要预留一部分老年代空间供用户线程使用,不能等到老年代几乎被填满时再收集。
- 如果预留空间不足,可能出现 “Concurrent Mode Failure”,导致虚拟机启用 Serial Old 收集器进行 Full GC,停顿时间较长。
- 内存碎片问题:
- CMS 使用标记-清除算法,导致内存中存在空间碎片。
- 为了解决碎片问题,CMS 提供了
-XX:+UseCMSCompactAtFullCollection
参数(JDK 9 开始废弃),在 Full GC 时进行内存碎片整理,但整理过程无法并发,导致停顿时间变长。 - 另一个参数
-XX:CMSFullGCsBeforeCompaction
(JDK 9 开始废弃)用于控制在进行若干次不整理空间的 Full GC 后,下一次 Full GC 前进行碎片整理。
Garbage First (G1)收集器:
优点
- 支持巨大的堆空间回收,并有较高的吞吐量。对比较大的堆延迟可控,如超过6G的堆回收时
- 不会产生内存碎片
- 并发标记的SATB算法效率高,比CMS的算法效率高
- 支持多CPU并行垃圾回收
- 允许用户设置最大暂停时间
JDK7引入,JDK9取代CMS成为默认垃圾收集器,至少要4G内存才推荐使用G1
G1是一款主要面向服务端应用的垃圾收集器。在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代 (Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。 而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
G1 不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、 Survivor 空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理
Region 中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis 指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region。
G1 收集器有以下细节:
-
每个Region都有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内,用于解决跨代引用问题
-
通过原始快照 解决 并发标记阶段 线程与用户线程同时进行 带来的 对象引用关系改变问题
-
程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把 Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。
-
三种GC模式,Young GC、Mixed GC和Full GC
Young GC
1、新创建的对象会存放在Eden区。当G1判断年轻代区不足(max默认60%),无法分配对象时需要回收时会执行Young GC。
2、标记出Eden和Survivor区域中的存活对象,
3、根据配置的最大暂停时间选择某些区域将存活对象复制到一个新的Survivor区中(年龄+1),清空这些区域。
4、后续Young GC时与之前相同,只不过Survivor区中存活对象会被搬运到另一个Survivor区。
5、当某个存活对象的年龄到达阈值(默认15),将被放入老年代。
6、部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,如果对象过大会横跨多个Region。
MixedGC
多次Young gc之后,会出现很多Old老年代区,此时总堆占有率达到阈值时(-XX:InitiatingHeapOccupancyPercent
默认45%)会触发混合回收Mixed GC,回收所有年轻代和部分老年代的对象以及大对象区。采用复制算法来完成。
混合回收分为如下步骤:
初始标记(initial mark)
并发标记(concurrent mark)
最终标记(remark或者Finalize Marking):只管漏标,不管新创建、不再关联的对象。这里使用的算法远比CMS的快
并发清理(cleanup):G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbage first)名称的由来。最后清理阶段使用复制算法,不会产生内存碎片。
Full GC
低延迟垃圾收集器:
Shenandoah和ZGC,几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定的,与堆的容量、堆中对象的数量没有正比例关系。实际上,它们都可以在任意可管理的(譬如现在 ZGC只能管理4TB以内的堆)堆容量下, 实现垃圾收集的停顿都不超过十毫秒这种以前听起来是天方夜谭、匪夷所思的目标。这两款目前仍处于实验状态的收集器,被官方命名为“低延迟垃圾收集器”
Shenandoah收集器:
RedHat开发的收集器,捐赠给OpenJDK,在OracleJDK中没有,主要是改进了G1收集器在回收阶段无法与用户进程并发的问题
ZGC 收集器:
JDK11推出
与Shenandoah和G1一样,ZGC也采用基于Region 的堆内存布局,但与它们不同的是,ZGC的Region具有动态性——动态创建和销毁,以及动态的区域容量大小。
- 小型Region:容量固定为 2MB,用于放置小于256KB的小对象
- 中型Region:容量固定为32MB,用于放置大于等于256KB但小于4MB的对象
- 大型Region:容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置4MB或以上的大对象。每个大型Region中只会存放一个大对象,大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段)的,因为复制一个大对象的代价非常高昂。
ZGC收集器有一个标志性的设计是它采用的染色指针技术,染色指针是一种直接将少量额外的信息存储在指针上的技术
Linux下64位指针的高18位不能用来寻址,但剩余的46位指针所能支持的64TB内存在今天仍然能够充分满足大型服务器的需要。鉴于此,ZGC的染色指针技术继续盯上了这剩下的46位指针宽度,将其高4位提取出来存储四个标志信息。通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到。由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂)
ZGC不通过原始快照或增量更新来实现并发标记,而是通过染色指针