jvm 垃圾收集器
一 关于垃圾收集器
垃圾收集器是垃圾回收算法(标记-清除算法、标记-复制算法、标记-整理算法、半复制算法)的具体实现,不同商家、不同版本的JVM所提供的垃圾收集器可能会有很在差别,本文主要介绍HotSpot虚拟机中的垃圾收集器。
相关术语
1. 并行与并发
并行(Parallel) | 并发(Concurrent) |
---|---|
指多条垃圾收集线程同一时间并行协同工作,此时用户线程仍然处于等待状态;如ParNew、Parallel Scavenge、Parallel Old; | 指用户线程与垃圾收集线程同时都在执行(但不一定是并行的,可能会交替执行);用户程序在继续运行,未被冻结,程序仍然能响应服务请求。垃圾收集程序线程占用系统资源。如CMS、G1(也有并行); |
2. 部分收集和整堆收集
(1)部分收集(Partial GC):
指目标不是完整收集整个Java堆的垃圾收集。主要三种:
A. 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
B. 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。
C. 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
(2)整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
注意:“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,请按资料上下文区分到底是指老年代的收集还是整堆收集。因为有的Major GC表示老年代GC有的表示整堆GC。
3. 收集器的指标
收集器的指标一般体现在设计它的目的上,如:Parallel Scavenge收集器首要目标是吞吐量。
主要权衡指标有三个:
(A)低延迟
(B)吞吐量
(C)较小的内存占用
二 Hotspot七种垃圾收集器
(图片来自《深入理解java虚拟机:jvm高级特性与最佳实践 》第三版。JVM系列黑白图均来自电子书,彩图来自网络)
图中是七种作用于不同分代的收集器。
如果两个收集器之间存在连线,就说明它们可以搭配使用。
图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。
其中Serial Old作为CMS出现”Concurrent Mode Failure”失败的后备预案。
整理成表格如下:
收集区域 | 收集器 |
---|---|
新生代收集器 | Serial、ParNew、Parallel Scavenge |
老年代收集器 | Serial Old、Parallel Old、CMS |
整堆收集器 | G1 |
1、Serial 收集器
Serial 串口,串行的意思。
Serial收集器是一个单线程工作的收集器。是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效。(高效是指与其他收集器的单线程相比)
特点:
(1)针对新生代的收集器。
(2)只会使用一个处理器或一条收集线程去完成垃圾收集工作,进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
2、ParNew 收集器
ParNew收集器实质上是Serial收集器的多线程并行版本。其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio
、-XX:PretenureSizeThreshold
、-XX:HandlePromotionFailure
等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。二者共用许多代码。
特点:
(1)新生代收集器
(2)支持多线程并行收集。
ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
3、Parallel Scavenge 收集器
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。早期只能和Serial Old搭配。不能和CMS搭配。
目标:
希望达到一个可控制的吞吐量(Throughput)。
Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。
特点:
(1)新生代收集器,基于标记-复制算法实现
(2)能够多线程并行收集
(3)停顿-吞吐量的自适应的调节策略。可以设定目标让收集器自行调节。
(4)支持NUMA(非统一内存访问架构)内存分配
运行示意图:
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的
-XX:MaxGCPauseMillis
参数以及直接设置吞吐量大小的-XX:GCTimeRatio
参数。
-XX:MaxGCPauseMillis
参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。
-XX:GCTimeRatio
参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。
停顿-吞吐量的自适应的调节策略:
Parallel Scavenge收集器还有一个参数
-XX:+UseAdaptiveSizePolicy
值得我们关注。这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio
)、晋升老年代对象大小(-XX:PretenureSizeThreshold
)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics).
把基本的内存数据设置好(如
-Xmx
设置最大堆),然后使用-XX:MaxGCPauseMillis
参数(更关注最大停顿时间)或-XX:GCTimeRatio
(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性。
4、Serial Old 收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。
特点:
(1)老年代收集器,
(2)单线程收集器,
(3)使用标记-整理算法。
使用环境:
客户端:主要供客户端模式下的HotSpot虚拟机使用。
服务端:(A)在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用;
(B)作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent ModeFailure时使用。
5、Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK 6时才开始提供的。
特点:
同Parallel Scavenge收集器。
“吞吐量优先”收集器:
在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
6、CMS 收集器
CMS : Concurrent Mark Sweep
官方文档称之为:“并发低停顿收集器”(Concurrent Low Pause Collector)
特点:
(1)以获取最短回收停顿时间为目标。
(2)基于标记-清除算法实现。
关注服务的响应速度,希望系统停顿时间尽可能短,如B/S系统应用服务端,适合CMS收集器。
CMS堆内存布局
CMS收集器工作过程:
运行示意图如下
1)初始标记(CMS initial mark)
(A)标记一下GC Roots能直接关联到的对象,速度很快。
(B)有短暂停顿
2)并发标记(CMS concurrent mark)
(A)从GC Roots的直接关联对象开始遍历整个对象图的过程。
(B)该过程耗时较长,用户线程可以与垃圾收集线程一起并发运行
3)重新标记(CMS remark)
(A)修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。
(B)有停顿,比初始标记停顿时间稍长,比并发标记阶段的时间短。
4)并发清除(CMS concurrent sweep)
(A)清理删除掉标记阶段判断的已经死亡的对象。
(B)不需要移动存活对象,可以与用户线程同时并发执行。
CMS收集器优缺点:
优点:
并发收集、低停顿
缺点:
(1)对处理器资源非常敏感,并发阶段线程运行占用资源。
(2)无法处理并发标记和并发清理阶段产生的“浮动垃圾”(Floating Garbage)。有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
(3)“标记-清除”算法实现带来的空间碎片问题。
浮动垃圾:在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”
因为浮动垃圾带来的问题怎么办?
CMS收集器不能像其他收集器那样等待到老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数
-XX:CMSInitiatingOccu-pancyFraction
的值来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。到了JDK 6时,CMS收集器的启动阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction
设置得太高将会很容易导致大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。
7、G1垃圾收集器
G1 : Garbage First
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
从JDK 6 Update 14开始就有Early Access版本的G1收集器供开发人员实验和试用,但由此开始G1收集器的“实验状态”(Experimental)持续了数年时间,直至JDK 7Update 4,Oracle才认为它达到足够成熟的商用程度,移除了“Experimental”的标识;到了JDK 8 Update 40的时候,G1提供并发的类卸载的支持,补全了其计划功能的最后一块拼图。这个版本以后的G1收集器才被Oracle官方称为“全功能的垃圾收集器”(Fully-FeaturedGarbage Collector)。
JDK9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。
设计思路:
追求能够应付应用的内存分配速率(Allocation Rate),而不追求一次把整个Java堆全部清理干净。
应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路从工程实现上看是从G1开始兴起的。
G1是收集器技术发展的一个里程碑。也是jdk11的默认垃圾收集器。
特点:
(1)面向服务端应用的垃圾收集器。
(2)基于Region的堆内存布局。
(3)使用Mixed GC模式,不再按“代”进行回收。(Region布局支持)
(4)遵循分代收集理论设计,新生代和老年代不再是固定大小与数理,只要求是逻辑连续的动态集合。
(5)回收策略是优先处理回收价值最大的Region
内存布局图:
G1不再是固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数
-XX:G1HeapRegionSize
设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region
之中,G1的大多数行为都把Humongous Region
作为老年代的一部分来进行看待
G1收集器实现思路:
(1)基于Region的堆内存布局,把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理。
(2)将堆分成多个独立Region后,使用记忆集避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集,记忆集会记录下别的Region指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种“双向”的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更复杂,同时由于Region数量比传统收集器的分代数量明显要多得多,因此G1收集器要比其他的传统垃圾收集器有着更高的内存占用负担。G1至少要耗费大约相当于Java堆容量10%至20%的额外内存来维持收集器工作。
(3)在并发标记阶段,通过原始快照(SATB)算法来实现用户线程改变对象引用关系时,不打破原本的对象图结构。
(4)在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。
可预测的“停顿时间模型”(Pause Prediction Model):
G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数
-XX:MaxGCPauseMillis
指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
-XX:MaxGCPauseMillis
-用户指定期望的停顿时间:
用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。不过,这里设置的“期望值”必须是符合实际的,不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。它默认的停顿目标为两百毫秒,一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常把望期停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
G1工作流程:
流程图如下:
1、初始标记(Initial Marking):
(A)只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
(B)该阶段需要停顿线程,但耗时很短。
初始标记是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
2、并发标记(Concurrent Marking):
(A)从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。
(B)这阶段耗时较长,可与用户程序并发执行。
(C)当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
3、最终标记(Final Marking)
(A)对用户线程做另一个短暂的暂停,处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
4、筛选回收(Live Data Counting and Evacuation)
(A)负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。
(B)操作涉及存活对象的移动,必须暂停用户线程。
(C)筛选回收由多条收集器线程并行完成的。
G1与CMS比较
G1 | CMS | |
不同 | G1基于Region布局,实现分代。 | CMS传统的分代布局。 |
G1可以设置最大停顿,关注点在吞吐量和延迟之间自行平衡或定制适应。 | CMS以最小停顿为关注点。 | |
G1按收益动态确定回收集。 | CMS传统的按代回收。 | |
G1整体基于“标记-整理”算法、局部(两个Region之间)基于“标记-复制”算法,运作期间不会产生内存空间碎片,利于程序长时间运行。 | CMS基于“标记-清除”算法,产生空间碎片,导致Full GC ,用户线程挂起。 | |
G1垃圾收集产生的内存占用(Footprint)和程序运行时的额外执行负载(Overload)。 【内存占用】:G1要维护堆中每个Region都有一份卡表。 【执行负载】:G1使用写前屏障来跟踪并发时的指针变化情况,实现原始快照搜索(SATB)算法;使用写后屏障来来更新维护结构更复杂的卡表,类似于消息队列的结构,是异步操作。 | CMS的内存占用(Footprint)和执行负载(Overload)相对低些。 【内存占用】:CMS只维护新生代与老年代之间唯一的一份卡表。 【执行负载】:CMS用写后屏障来更新维护卡表,是同步操作。 | |
G1相对适合大内存应用。 | CMS相对适合小内存应用。临界(6G~8G) | |
相同 | (1)都非常关注停顿时间的控制。 (2)都使用传统的分代理论。 (3)都使用卡表来处理跨代指针问题。 |
三 低延迟垃圾收集器
其中 Shenandoah 和 ZGC ,这两款目前仍处于实验状态的收集器,被官方命名为“低延迟垃圾收集器”(Low-Latency Garbage Collector或者Low-Pause-Time GarbageCollector)。
1、Shenandoah 收集器
主要目标:
实现一种能在任何堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的垃圾收集器。
特点:
(1)基于Region的堆内存布局
(2)回收策略优先处理回收价值最大的Region
(3)支持并发的整理算法。
三个重要的并发阶段:
并发标记、并发回收、并发引用更新。
具体更详细的学习笔记:《jvm垃圾收集器Shenandoah》
2、ZGC 收集器
主要目标:
尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
ZGC主要特征:
(1)以低延迟为首要目标。
(2)暂不支持分代的。
(3)基于Region的堆内存布局。
(4)染色指针实现并发整理算法。Colored Pointer。
(5)内存多重映射技术。
(6)支持“NUMA-Aware”的内存分配。(NUMA非统一内存访问架构)
具体更详细的学习笔记:《jvm垃圾收集器ZGC》