G1回收器
G1(Garbage First)收集器
背景:随着硬件资源的不断升级,可用的内存资源越来越多,这对于垃圾收集器的发展提出了新的挑战。
传统的垃圾收集器采用物理分区的方式将内存分为老年代、新生代、永久代或 MetaSpace,但随着可用内存的增加,某一分代区域的大小可能会达到几十上百 GB。
在这种情况下,传统的物理分区收集方式会导致垃圾扫描和清理时间变得更长,性能下降。
它的设计目标是为了适应不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量。
Region(局部收集)
CMS的继承者 : G1 垃圾收集器吸取了 CMS 垃圾收集器的优良思路,并通过摒弃物理分区、采用 Region 分区的方式,实现了更细粒度的垃圾回收,从而提高了整个系统的性能和可用性。
G1 垃圾收集器的最核心分区基本单位是 Region。
G1将堆内存“化整为零”的解题思路,G1 垃圾收集器摒弃了传统的物理分区方式,而是将整个内存分成若干个大小不同的 Region 区域。
G1垃圾收集器把Java堆划分为2048个大小相等的独立的Region,每个Region大小取值范围为1-32MB,且必须为2的N次幂参数,
可以通过-XX:G1HeapRegionSize设定,-XX:G1HeapRegionSize默认为0,此时Region的大小采用Java堆大小/2048的计算公式来确定,取值为最接近的2的N次幂数值。
Region有四种:Eden、Survivor、Old、Humongous(用于存储超过1.5个region的大对象),空白表示未使用区域。
Humongous:部分对象如果大小超过Region的一半,会直接放入老年代,这类老年代被称为Humongous区。
比如堆内存是4G,每个Region是2M,只要一个大对象超过了1M就被放入Humongous区,【如果对象过大会横跨多个Region】。
GC算法:复制
特点如下:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。
部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。 - 分代收集: G1依然属于分代垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden和Survivor,但是从堆的结构上看,年轻代和老年 代不再物理隔离,而是逻辑上的概念;
不要求整个eden、survivor区连续的,也不再坚持固定大小和数量。 - 空间整合:G1运作期间不会产生空间碎片,收集后能提供规整的可用内存,有利于程序长时间运行,分配大对象时不会因为无法找到连续的内存空间而提前触发下一次GC。
- 可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。
能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
可以通过参数-XX:MaxGCPauseMillis设置
G1去跟踪各个Region里面垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,
然后在后台维护一个优先级列表,根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region,也就是“Garbage First”名字的由来。
G1为什么能建立可预测的停顿时间模型?
因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
这样就保证了在有限的时间内可以获取尽可能高的收集效率。
如何识别和处理老年代对象对年轻代对象的引用?
记忆集(RememberedSet)
RSet详细记录了夸Region引用关系。
RSet是一种points-into结构(谁引用了我),实际实现是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是卡表(Card Table)的Index。
卡表(Card Table)
G1为所有的Region维护了一张全局的卡表(Card Table),它的核心作用是记录跨代引用(老年代对象引用年轻代对象)。
卡表的每个卡(Card)通常是一个字节,标记为1表示“脏”,即该卡对应的内存块(512B)有老年代对象引用了年轻代对象。
一个 Region 就包含多个 Card,Card 是 G1 进行内存管理和垃圾回收的最小单位。一个对象可能跨越多个 Card,或者一个 Card 内存储多个对象
写屏障(Write Barrier):当发生跨代引用时,写屏障会触发卡表的标记操作,将对应的卡标记为“脏”。
RSet的更新:G1的每个Region的RSet通过卡表的脏卡信息,间接记录哪些Region的哪些卡页引用了当前Region的对象。
记忆集的粒度更精细,避免全量扫描老年代(避免全量扫描在卡表中被标记为脏卡的卡页中的所有对象)。
收集集合 CSet
代表每次 GC 暂停时回收的一系列目标分区,在任意一次收集暂停中,CSet 所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。
年轻代收集 CSet 只容纳年轻代分区,而混合收集会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到 CSet 中
GC过程
Young GC:仅回收年轻代(Eden和Survivor区),这个过程是Stop-The-World(STW)的
- 扫描根:根引用连同 RSet 记录的外部引用作为扫描存活对象的入口
- 更新 RSet:处理 dirty card queue 更新 RSet,此后 RSet 准确的反映对象的引用关系
a.dirty card queue:类似缓存,产生了引用先记录在这里,然后更新到 RSet
b.作用:产生引用直接更新 RSet 需要线程同步开销很大,使用队列性能好 - 处理 RSet:识别被老年代对象指向的 Eden 中的对象,这些被指向的对象被认为是存活的对象,把需要回收的分区放入 Young CSet 中进行回收
- 复制对象:Eden 区内存段中存活的对象会被复制到 survivor 区,survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1
达到阀值会被会被复制到 old 区中空的内存分段,如果 survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间 - 处理引用:处理 Soft,Weak,Phantom,JNI Weak 等引用,最终 Eden 空间的数据为空,GC 停止工作
并发标记周期(Concurrent Marking Cycle):
1.初始标记(Initial Mark):标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC
2.并发标记(Concurrent Marking):
在整个堆中进行并发标记(应用程序并发执行),可能被 YoungGC 中断。
会计算每个区域的对象活性,即区域中存活对象的比例,若区域中的所有对象都是垃圾,则这个区域会被立即回收(实时回收),给浮动垃圾准备出更多的空间,把需要收集的 Region 放入 CSet 当中
3.最终标记(Final Marking)
为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,
最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行(防止漏标)
4.清理(Cleanup)阶段
并发清理阶段,首先对 CSet 中各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,也需要 STW
并发标记周期完成后,才会触发混合回收(Mixed GC),该阶段会回收部分年轻代和老年代Region。
Mixed GC:当老年代占用率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Eden区、Survivor区和部分Old区(根据期望的GC停顿时间和回收收益确定Old区垃圾收集的优先顺序)以及大对象区。
Full GC:在Mixed GC 回收的内存不够的时候触发Full GC,回收全堆。单线程执行标记-整理算法,此时会导致用户线程的暂停。
所以尽量保证应该用的堆内存有一定多余的空间。
G1与其他收集器的区别:
(1)其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。
(2)在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。
(3)虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。