经典垃圾收集器
- Serial
- ParNew
- Parallel Scavenge
- Serial Old
- Parallel Old
- CMS
- G1 (Garbage First)
- 参数
Serial
它是最基础、最悠久的一款新生代收集器。它是一个单线程工作的收集器,在收集的时候会暂停用户线程(STW)。
优点:
它优于其他收集器的地方就是简单高效,对于内存资源受限的环境,它是所有收集器中额外内存中最小的。所以,它对运行在客户端模式下的虚拟机是一个很好的选择
Serial/Serial Old 收集器收集过程
ParNew
它实际上是Serial收集器的多线程并行版本。除了Serial收集器,只有它能和 CMS 收集器配合工作。
ParNew/Serial Old 收集过程
Parallel Scavenge
跟ParNew一样,它是一款新生代收集器,同样基于标记-复制算法,也是能够并行收集的多线程收集器。
它的特点就是它与其他收集器关注点不同, CMS 等收集器关注重点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量 (ThroughPut)。
吞吐量
吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值 \(吞吐量 = 运行用户代码时间\div \\{运行用户代码时间 + 运行垃圾收集时间}\) 高吞吐量可以最高效率的利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
参数
-XX:MaxGCPauseMillis
最大垃圾收集停顿时间,允许为一个大于0的毫秒值,配置后收集器将尽可能保证内存花费时间不超过用户的设定值。(注意:垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的)
-XX:GCTimeRatio
参数值应该设置为一个正整数,表示用户期望虚拟机消耗在GC的时间不超过程序总运行时间的 1/(1+N)。默认值为99,即收集器的时间消耗不超过总运行时间的1%
-XX:+UseAdaptiveSizePolicy
该参数激活后,就不需要人工指定新生代大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX: PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统运行情况收集性能监控信息,动态调整这些参数已提供合适的停顿时间或者最大的吞吐量。
Serial Old
Serial Old是Serial 收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。
使用场景
客户端模式下的hotspot虚拟机使用。
服务端:
- JDK5以及之后版本与Parallel Scavenge 收集器搭配使用
- 作为CMS收集器 (基于标记-清除) 收集失败后的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
Parallel Old
Parallel Old 直到JDK6才开始提供,是Parallel Scavenge收集器的老年代版本,支持多线程并行收集,基于标记-整理算法实现。
使用场景
在注重吞吐量或者处理器资源较为稀缺的场景,都可以考虑 Parallel Scavenge + Parallel Old 收集器的组合。
Parallel Scavenge/ Parallel Old 收集过程
CMS
CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法。
只有它会单独回收老年代
收集过程
初始标记 (initial mark)
仅仅标记一下GC Roots能直接关联到的对象,速度很快
并发标记(concurrent mark)
可以跟用户线程并发运行,从GC Roots直接关联对象开始遍历整个对象图。由于跟用户线程并发运行的关系,标记期间老年代所产生的新生代引用会被记录在一个记忆集(Remembered Set),关于记忆集可以通过 三色标记算法 解. 通过记忆集使用增量更新避免“对象消失“的问题.
重新标记(remark)
修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。停顿时间会比初始标记稍微长一点,但也远比并发标记时间短。
并发清除(concurrent sweep)
清理掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是与用户线程同时并发的
缺点
对处理器资源非常敏感
在并发阶段,虽然不会导致用户线程停顿,但会因为占用一部分线程导致应用程序变慢。CMS默认启动回收线程数是 (处理器核心数量+3)/4, 也就是说如果处理器核心数量在4个或4个以上,并发回收时垃圾收集线程会占用不少于25%的处理器运算资源,但是当处理器不足四个时,CMS对用户程序影响就可能变得很大。
无法处理浮动垃圾
并行期间,程序运行会产生新的垃圾对象,但是这一部分垃圾对象是在标记过程结束以后产生,所以无法在当次回收时清除,就会导致产生“浮动垃圾”的产生。要是CMS运行期间预留的内存无法满足程序分配,就会产生并发失败“Concurrent Mode Failure” 而导致另一次完全 “STW” 的FULL GC产生
空间碎片大对象
基于标记-清除算法CMS回收不会移动对象,意味着在回收结束后会产生大量空间随便。空间碎片过多时,大对象分配会比较麻烦,当无法找到足够大的连续空间分配大对象,就不得不出发一次FULL GC (根据参数配置full gc进行内存整理).
参数
-XX:CMSInitiatingOccuPancyFraction
CMS会在并发收集时预留一部分空间以支持用户线程运行,该参数可用来配置CMS的出发百分比。JDK5默认设置为68%,也就是老年代使用了68%空间后就会被激活。JDK6时默认配置为92%。
注意:该值设置过高很容易导致大量并发失败产生。
-XX:+UseCMSCompactAtFullCollection (JDK9开始废弃)
用于CMS收集器不得不进行Full GC 时开启内存碎片的合并整理过程。
-XX: CMSFullGCsBeforeCompaction(JDK9开始废弃)
要求CMS收集器在执行若干次(参数值)不整理空间的FULL GC后,下一次进入 FULL GC前会先进行碎片整理。默认为0,也就是每次进入Full Gc之前会进行碎片整理
G1 (Garbage First)
内存布局
基于Region的堆内存布局。将连续的Java堆划分成多个大小相等的独立区域(Region),
每一个Region都可以根据需要扮演新生代的Eden、Survivor或者老年代空间。还有一类特殊的Humongous区域,专门用来存储大对象,只要是大小超过一个Region容量的一半即判定为大对象,Region大小可通过-XX:G1HeapRegionSize
设定。对于大小超过整个Region容量的对象,将会存放在N个连续的Humongous Region之中。G1的大多数行为会把Humongous
Region作为老年代的一部分来看待.
停顿预测模型(实现Mixed GC)
实现:
- 垃圾收集目标范围不再分代,可以面向堆内存任何部分组成回收集(Collection Set, 简称 CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放垃圾数量最多,回收收益最大。
- 将Region作为单次回收的最小单元,这样可以有计划的避免在整个Java堆中进行全区域的垃圾收集。跟踪各个Region垃圾堆积价值(
回收所获得的空间大小以及所需时间的经验值)
大小,后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用
-XX:MaxGCPauseMillis
指定,默认为200毫秒)优先处理回收价值收益最大的Region.
使用Region划分内存空间以及具有优先级的区域回收方式,保证了G1收集器在有限时间内获取尽可能的收集效率。
G1收集器关键细节
Q1: java堆拆分为多个Region后,Region里面的跨代引用对象如何解决?
在每个Region中都维护自己的记忆集,使用记忆集避免全堆作为GC Roots扫描。这些记忆集会记录别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。 由于维护这么多记忆集,G1相比其他传统收集器有更高的内存占用负担。根据经验,G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。
Q2: 并发标记阶段如何保证收集线程与用户线程互不干扰的运行?
使用原始快照(SATB)算法来实现的(具体可参考 三色标记算法#1.3 漏标(对象消失)-读写屏障)
G1为每一个Region设计了两个名为TAMS(TOP at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发会收拾新分配到对象地址都必须在这两个指针位置以上。
在TAMS指针地址以上的对象默认被隐式标记过的,默认存活,所以不会回收,与CMS的‘‘Concurrent Mode Failure’’失败会导致Full GC类似,如果内存回收赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。
Q3: 怎样建立可靠的停顿预测模型? 用户通过 -XX:MaxGCpauseMillis
参数指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1该怎么做才能满足用户期望呢?
G1收集器的停顿预测模型是以衰减均值(Decaying Average)
为理论基础来实现的,垃圾收集过程中,G1会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测当前回收时由哪些Region组成的回收集可以在不超过期望停顿时间的约束下获得最高的收益。
Minor GC 回收步骤
G1的Minor GC其实触发时机跟前面提到过的垃圾收集器都是一样的,等到Eden区满了之后,会触发Minor GC。Minor GC同样也是会发生Stop The World的。 Minor GC我认为可以简单分为为三个步骤
根扫描
第一步应该很好理解,因为这跟之前CMS是类似的,可以理解为初始标记的过程,
更新&&处理 RSet
它是通过「卡表」(cart table)来避免全表扫描老年代的对象,因为Minor GC 是回收年轻代的对象,但如果老年代有对象引用着年轻代,那这些被老年代引用的对象也不能回收掉,同样的,在G1也有这种问题(毕竟是Minor GC)。CMS是卡表,而G1解决「跨代引用」的问题的存储一般叫做RSet,只要记住,RSet这种存储在每个Region都会有,它记录着「其他Region引用了当前Region的对象关系」 ** 对于年轻代的Region,它的RSet 只保存了来自老年代的引用(因为年轻代的没必要存储啊,自己都要做Minor GC了),而对于老年代的 Region 来说,它的 RSet 也只会保存老年代对它的引用(在G1垃圾收集器,老年代回收之前,都会先对年轻代进行回收,所以没必要保存年轻代的引用), **无非就是处理RSet的信息并且扫描,将老年代对象持有年轻代对象的相关引用都加入到GC Roots下,避免被回收掉,
复制对象
到了第三步也挺好理解的:把扫描之后存活的对象往「空的Survivor区」或者「老年代」存放,其他的Eden区进行清除, cset,它的全称是 Collection Set,保存了一次GC中「将执行垃圾回收」的Region。CSet中的所有存活对象都会被转移到别的可用Region上。
此小节转载链接:https://juejin.cn/post/7048977673091022856
Mixed GC回收步骤
堆空间的占用率达到一定阈值后会触发Mixed GC(默认45%,由参数决定) Mixed GC 依赖「全局并发标记」统计后的Region数据,全局并发标记」它的过程跟CMS非常类型,步骤大概是:初始标记(STW)、并发标记、最终标记(STW)以及清理(STW), Mixed GC它一定会回收年轻代,并会采集部分老年代的Region进行回收的,所以它是一个混合GC。 链接:https://juejin.cn/post/7048977673091022856
初始标记(Initial Marking)
仅标记GC Roots能直接关联的对象,并修改TAMS指针的值,为并发标记阶段能正确的在可用Region中分配新对象。
该阶段需要暂停用户线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记(Concurrent Marking)
从 GC Root开始扫描整个堆中的对象图,根据可达性分析( 三色标记算法#1.1 基本算法 找到需要回收的对象。
此阶段耗时比较长,但是可与用户线程并发运行。
最终标记(Final Marking)
对用户线程进行一个短暂的暂停,处理SATB
记录下的在并发时有引用变动的对象。
筛选回收(Live Data Counting and Evacuation)
更新Region的统计数据
,对每个Region的回收价值和成本
进行排序,根据用户所期望的停顿时间
来制定回收计划,选择任意多个Region
构成回收集,将决定回收的那部分Region中存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。
由于涉及到存活对象移动,此阶段需要暂停用户线程,有多条收集器线程并行处理。
优点:
- 并非纯粹追求低延迟,它是在延迟可控的情况下获得尽可能高的吞吐量
- 回收过程算法整体看基于
标记-整理
,局部看基于标记-复制
, 这两种算法都不会产生空间碎片,垃圾收集完能提供规整的可用内存。
缺点
-
垃圾收集的内存占用(Footprint) 比CMS高。
G1的卡表实现很复杂,而且堆中每一个Region无论老新年代都必须维护一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占用整个堆20%乃至更多的内存空间,CMS卡表维护就相当简单,只有唯一一份且只处理老年代到新生代的引用(代价就是当CMS发生Old GC时,要把整个新生代当做GC roots扫描)
-
程序运行额外执行负载(Overload)要比CMS高
G1除了使用
写后屏障
进行卡表维护外,为了实现原始快照SATB
算法,还使用了写前屏障
来跟踪并发时指针变化情况。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写后屏障实现是直接的同步操作,而G1就不得不将其实现为类似消息队列的结构,把写前写后屏障要做的事件放入队列中异步处理。
CMS对比
目前在小内存应用上CMS表现大概率优于G1, 而大内存应用G1则能发挥其有事,这个优劣势的Java堆容量平衡点通常在6GB~8GB之间。
记忆集
G1的记忆集本质是一个hash表, Key是别的Region的起始地址,Value是一个集合,里边存储的元素是卡表的索引号。
参数
-XX:G1HeapRegionSize
设置Region大小, 取值范围1MB~32MB,且应该为2的N次幂。
-XX:MaxGCpauseMillis
设置允许的收集停顿时间,默认为200毫秒
参数
-XX:+/-UseParNewGC
jdk9之后移除, 强制指定或者禁用 ParNew
-XX:+UseConcMarkSweepGC
激活CMS