一、Garbage First简介
简称G1,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。G1是一款主要面向服务端应用的垃圾收集器。HotSpot开发团队最初赋予它的期望是未来可以替换掉JDK 5中发布的CMS收集器。现在这个期望目标已经实现过半了,JDK 9发布之日,G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则沦落至被声明为不推荐使用(Deprecate)的收集器。如果对JDK 9及以上版本的HotSpot虚拟机使用 参数-XX:+UseConcMarkSweepGC来开启CMS收集器的话,用户会收到一个警告信息,提示CMS未来将会被废弃。
二、CMS面临被取代
1、CMS与它的兄弟们
但作为一款曾被广泛运用过的收集器,经过多个版本的开发迭代后,CMS(以及之前几款收集
器)的代码与HotSpot的内存管理、执行、编译、监控等子系统都有千丝万缕的联系,这是历史原因导致的,并不符合职责分离的设计原则。这是这紧密的关系,导致CMS未被完全取代,但是开发者们想尽办法让G1取代CMS。
2、统一垃圾收集器接口的出现
为此,规划JDK 10功能目标时,HotSpot虚拟机提出了“统一垃圾收集器接口”,将内存回收的“行为”与“实现”进行分离,CMS以及其他收集器都重构成基于这套 接口的一种实现。以此为基础,日后要移除或者加入某一款收集器,都会变得容易许多,风险也可以控制,这算是在为CMS退出历史舞台铺下最后的道路了。
3、停顿时间模型的出现
作为CMS收集器的替代者和继承人,设计者们希望做出一款能够建立起“停顿时间模型”(Pause
Prediction Model)的收集器,停顿时间模型的意思是能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSJ)的中软实时垃圾收集器特征了。
三、停顿时间模型的实现
1、面向堆内存任何部分进行回收
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大。
2、基于Region的堆内存布局
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。
3、Region中的Humongous区域
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代一部分来进行看待。
4、Garbage First名字的来源
虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区
域(不需要连续)的动态集合。G1收集器之所以能建立可预测的停顿时间模型,是因为它将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍,这样可以有计划地避免在整个Java堆中进行全区域的垃圾收集。更具体的处理思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。
从2004年Sun实验室发表第一篇关于G1的论文后一直拖到2012年4月JDK 7 Update 4发布,用将近10年时间才倒腾出能够商用的G1收集器来。可见其中有许多的困难问题。
四、G1实现在实现时的困难(了解)
问题一:将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?
1、跨带引用假说、记忆集
跨带引用相对于同代引用仅占少数。
根据这条假说,就没有必要去为了少量的跨带引用去扫描整个老年代,也不用浪费空间去记录每个对象是不是存在哪些跨域引用,只用在新生代上建立一个全局的数据结构(记忆集),它把老年代划分成若干小块,标识出老年代的哪一部分存在跨域引用。在发生MinorGc时,只用包含了跨带引用的小块内存里的对象才会被加到GC Roots里面进行扫描。
解决办法:
使用记忆集避免全堆作为GC Roots扫描,但是在G1的应用上要复杂的多,每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针。
问题二:在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,导致标记结果出现错误,该问题的解决办法:CMS收集器采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法来实现的。此外,垃圾收集对用户线程的影响还体现在回收过程中新创建对象的内存分配上,程序要继续运行就肯定会持续有新对象被创建,G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。G1收集器默认在这个地址以上的对象是被隐式标记过的,即默认它们是存活的,不纳入回收范围。与CMS中的“Concurrent Mode Failure”失败会导致Full GC类似,如果内存回收的速度赶不上内存分配的速度,G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。
问题三:怎样建立起可靠的停顿预测模型?
用户通过-XX:MaxGCPauseMillis参数(用户设定允许的收集停顿时间)指定的停顿时间只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。
五、G1的运作过程
如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的 运作过程大致可划分为以下四个步骤:
一、初始标记
仅标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这阶段要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。
二、并发标记
从GC Roots 能直接关联到的对象开始进行可达性分析,递归扫描整个堆里的对象图,找到要回收的对象,耗时较长,与用户程序同时执行,对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
SATB:SATB中的指的是在并发标记阶段开始时对堆内存进行的一次快照,记录下此时存活的对象。
三、最终标记
对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
四、筛选回收
负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
G1除了并发标记以外,其余的阶段也是要暂停用户线程的,换言之,他并不是纯粹的追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。
六、用户自己指定期望的停顿时间
G1强大的功能在于,他可以由用户自己指定期望的停顿时间,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。
不过,这里设置 的“期望值”必须是符合实际的,不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。它默认的停顿目标为两百毫秒,一般来说,回收阶段占到几十到一百甚至接近两百毫秒都很正常,但如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。很可能一开始收集器还能从空闲的堆内存中获得一些喘息的时间,但应用运行时间一长就不行了,最终占满堆引发Full GC反而降低性能,所以通常把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。
为什么说G1是收集器技术发展的一个里程碑?
从G1开始,最先进的垃圾收集器的设计导向都不约而同地变为追求能够应付应用的内存分配速率 (Allocation Rate),而不追求一次把整个Java堆全部清理干净。这样,应用在分配,同时收集器在收集,只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。这种新的收集器设计思路从工程实现上看是从G1开始兴起的,所以说G1是收集器技术发展的一个里程碑。
七、CMS和G1的比较
一、G1
优点:
指定最大停顿时间,分Region的内存区域,按收益动态确定回收集,与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”算法实现的收集器,但从局部(两个Region之间)上看又是基于“标记-复制”算法实现,无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。这种特性有利于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。
缺点:
用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载 (Overload)都要比CMS要高。
二、卡表和卡页
卡表是Java虚拟机(JVM)中用于优化垃圾收集过程的一种数据结构。
在JVM的垃圾收集过程中,卡表(Card Table)扮演着重要的角色。它主要用于记录堆内存中老年代到新生代的引用信息,以便在新生代垃圾收集(Minor GC)时能够快速确定哪些对象是可达的,从而避免扫描整个老年代的开销。具体来说,卡表的设计基于将堆空间划分为一系列固定大小的卡页(Card Page),每个卡页通常为2的幂次方字节大小(如512字节)。卡表中的每个元素对应一个卡页,用于标记该卡页是否包含跨代引用。当老年代的对象引用了新生代的对象时,对应的卡表项会被标记为dirty。
在Minor GC发生时,垃圾收集器会首先检查卡表中的dirty项,只有那些被标记为dirty的卡页中的对象才会被视为GC Roots的一部分,并参与后续的可达性分析。这样,就大大减少了需要扫描的内存区域,提高了垃圾收集的效率。
需要注意的是,虽然卡表机制能够显著提高垃圾收集的性能,但它也引入了一些额外的开销。例如,每次对引用进行写操作时,都需要更新卡表的状态,这可能会增加一些运行时的开销。此外,在高并发环境下,频繁的写屏障操作还可能导致虚共享问题,进而影响程序性能。
三、内存占用方面比较
就内存占用来说,虽然G1和CMS都使用卡表来处理跨代指针,但G1的卡表实现更为复杂,而且堆中每个Region,无论扮演的是新生代还是老年代角色,都必须有一份卡表,这导致G1的记忆集(和其他内存消耗)可能会占整个堆容量的20%乃至更多的内存空间;相比起来CMS的卡表就相当简单,只有唯一一份,而且只需要处理老年代到新生代的引用,反过来则不需要,由于新生代的对象具有朝生夕灭的不稳定性,引用变化频繁,能省下这个区域的维护开销是很划算的。
四、执行负载的角度
在执行负载的角度上,同样由于两个收集器各自的细节实现特点导致了用户程序运行时的负载会
有不同,譬如它们都使用到写屏障,CMS用写后屏障来更新维护卡表;而G1除了使用写后屏障来进行 同样的(由于G1的卡表结构复杂,其实是更烦琐的)卡表维护操作外,为了实现原始快照搜索(SATB)算法,还需要使用写前屏障来跟踪并发时的指针变化情况。相比起增量更新算法,原始快照搜索能够减少并发标记和重新标记阶段的消耗,避免CMS那样在最终标记阶段停顿时间过长的缺点,但是在用户程序运行过程中确实会产生由跟踪引用变化带来的额外负担。由于G1对写屏障的复杂操作要比CMS消耗更多的运算资源,所以CMS的写屏障实现是直接的同步操作,而G1就不得不将其实现为类似于消息队列的结构,把写前屏障和写后屏障中要做的事情都放到队列里,然后再异步处理。
五、写屏障
在Java虚拟机(JVM)中,写屏障是一种用于确保在并发垃圾回收过程中对象的引用关系能够被正确追踪和管理的机制。
写屏障的核心作用是在程序修改对象引用时插入一段额外的逻辑,以便垃圾回收器能够实时捕捉到这些变化,从而避免漏标或误标对象。在并发标记垃圾回收算法中,写屏障尤为重要,因为它帮助解决了在并发环境下对象引用关系动态变化的问题。
写屏障有两种常见的模式:
前写屏障(Pre-write Barrier):在对象引用被修改之前执行,记录引用变更前的信息,主要用于保持旧的引用关系,以防止重要对象被过早回收。
后写屏障(Post-write Barrier):在对象引用被修改之后执行,确保新的引用对象被正确追踪,更常见于三色标记算法的实现中。
写屏障通常与三色标记算法一起使用,该算法将内存中的对象分为三类:白色(尚未访问到的对象)、灰色(已访问但其引用对象还未完全处理的对象)和黑色(已完全处理的对象)。当程序运行时,如果修改了一个已标记为“黑色”的对象,使其指向了一个“白色”对象,写屏障会将该“白色”对象标记为“灰色”,以确保垃圾回收器不会忽略这个新引用。
看看就行了,别真记住了....