G1垃圾收集器原理

  |   0 评论   |   0 浏览

G1(Garbage-First)垃圾收集器是一个具有标志性意义的垃圾收集器,有别于CMS等前代的垃圾收集器,它采取了全新的垃圾收集思路,开创了面向局部收集的设计思路和基于Region的内存布局形式。前代的所有包括CMS在内的其他收集器,垃圾收集的目标范围是整个新生代(Minor GC)、整个老年代(Major GC)或者是整个Java堆(Full GC)。而G1收集器可以面向堆内存任何部分来组成回收集CSet(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多并且回收收益最大,这也是G1的Garbage First名字的由来。

1.G1的重要概念

1.1 垃圾收集器的内存分区

在G1之前的垃圾收集器统一将内存分成新生代,老年代和持久代,并且分代执行垃圾回收算法。

图1:传统的分代内存分区

G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。G1把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间和Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中。

图2:G1的内存分区

1.2 可达性分析算法

垃圾收集器所面临的的第一个问题是如何确定一个对象是不是垃圾。通常认为一个不可能再经过任何途径被使用的对象是垃圾,目前各大常用语言使用的找出这一类无法使用的对象的方法是可达性分析算法。

1.2.1 算法概要

可达性分析算法的基本思想是通过一系列称为 " GC Roots " 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象与 GC Roots 之间没有任何引用链相连(即 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。

在Java语言中,可作为GC Roots的对象包含以下几种:

(a)虚拟机栈(栈帧中的本地变量表)中引用的对象。(可以理解为:引用栈帧中的本地变量表的所有对象)

(b)方法区中静态属性引用的对象(可以理解为:引用方法区该静态属性的所有对象)

(c)方法区中常量引用的对象(可以理解为:引用方法区中常量的所有对象)

(d)本地方法栈中(Native方法)引用的对象(可以理解为:引用Native方法的所有对象)

在可达性分析算法中,即使是从" GC Roots " 对象出发不可达的对象,也并非一定是垃圾,要真正宣告一个对象是垃圾,至少要经历再次标记过程。一次标记是可达性分析不可达,另一次标记是对象没有必要执行 finalize() 方法,或者在 finalize() 方法中对象无法再与引用链上的任何一个对象重新建立起关联。

可达性分析算法的不足

可达性分析算法在多线程的场景下会有安全问题,在多线程环境下,垃圾回收线程并不会阻塞其他程序的线程,是与当前用户程序并发执行的。所以当GC线程标记好了一个对象之后,用户程序的线程又将对象重新加入了“GC Roots”的引用链中,当执行二次标记的时候,该对象也没有重写finalize()方法,因此垃圾回收的时候就会回收这个不该回收的对象。

对于上述不足,虚拟机最根本的解决方法就是在一些特定指令位置设置一些“安全点”,当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程(该做法被称为Stop The World ),暂停后再进行“GC Roots”根节点枚举,进而执行垃圾标记操作。 所谓的Stop the World(STW)机制就是在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。

当然,解决这个问题不仅仅是用了STW这么简单就可以了,原因就是STW很影响用户程序的执行性能,除非逼不得已时垃圾处理器才会使用STW,在其他的情况下会用效率更高的方式来解决这个问题,比如G1的原始快照SATB策略,这个后续会讲。

1.2.2 安全点和安全区域

1.2.2.1 安全点

在程序执行的过程中,GC 线程也不是时时刻刻都能进行GC的,因为GC时的一些必要步骤是伴随着STW的,此时用户线程会被暂停并挂起,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化(比如刚好在new这个关键字,你暂停了,这个对象是有还是没有,这个对象有没有引用关联着等情况都比较麻烦处理)。所以 JVM 会在字节码指令中,选一些指令,作为“安全点”,必须得在安全点(Safepoint)处才能进行GC。安全点位置的是以“是否具有让程序长时间执行的特征”为标准进行选定的。由于每条指令执行的时间都非常短暂,因此能够“长时间执行”的指令一般就是复用的指令序列,因此通常会将如「方法调用、循环跳转、异常跳转」等指令序列复用作为安全点。

解决了什么指令可以充当安全点的问题之后,还需要解决另外一个问题:一个应用程序中,同一时刻可能有很多个线程在执行,但是所有线程在同一时刻都遇到安全点的概率太小了。通常情况下,当要发生 GC 时,可能是其中一部分线程遇到了安全点,而另一部分线程还没到安全点,那这个时候该怎么办呢?答案是让其他线程也都跑到最近的安全点停下来。

而让其他线程都跑到最近点的安全点停下来有两种做法:

(1)「抢占式中断」,也就是中断所有的线程,然后再判断每个线程是否在安全点,如果不在,就恢复线程,让线程继续运行,直到跑到最近的安全点,这种方式目前已经没有虚拟机在采用了。

(2)「主动式中断」,当要发生 GC 时,设置一个标志位,然后其他线程运行到安全点时,先判断这个标志位,如果标志位的值表示此时需要进行 GC,那么线程就停下来,将自己中断挂起,否则继续运行。

1.2.2.2 安全区域

安全点的存在使得程序在运行时,在不太长的时间内,就会遇到可以进入 GC 的安全点,但是如果遇到线程”不执行“的情况该怎么办呢?所谓的程序不执行就是没有分配处理器时间,典型的场景便是用户线程处于Sleep状态或者Blocked状态,这时候线程无法响应虚拟机的中断请求,不能再走到安全的地方去中断挂起自己,虚拟机也显然不可能持续等待线程重新被激活分配处理器时间。对于这种情况,就必须引入安全区域(Safe Region)的概念来解决。

安全区域是指:在一个代码片段内,对象之间的引用关系不会发生变化,那么在这个区域中的任何位置进行垃圾回收都是没问题的,这个代码片段可以看做安全区域,其实安全区域就是一个拉长了的安全点。

在实际执行过程中,当线程进入到安全区域时,会先将自己标识为进入 Safe Region 状态,那么当需要进行 GC 时,JVM 会忽略已经进入到 Safe Region 的线程;当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止。

1.3 记忆集和屏障

由于G1垃圾收集器将内存分成了不同的region,因此在垃圾标记和回收时,不可避免的会有跨Region引用的情况产生:一个region中存储的对象可能被其他任意region(这些region可能Old区或者Eden区)中的对象所引用。这样一来,在进行YGC的时候,在判断Eden区中的一个对象是否存活时,需要去扫描所有的region(包括Old区,Eden区等),导致了在回收年轻代的时候,还需要扫描老年代,同时扫描表示所有Eden区和Old区的region,相当于做了一个全堆扫描,这会大大降低YGC的效率。(在CMS等分代回收的垃圾回收器中,也存在跨代引用的问题,即如果老年代对象引用了新生代的对象,那么回收新生代时需要扫描从老年代到新生代的所有引用)

为了解决上述问题,通常采用记忆集(Remembered Set,RSet)来避免全堆扫描。记忆集在不同的垃圾收集器中的实现方式不同,G1采用卡表的形式来实现记忆集,具体方式如下:

(1)G1将Java堆划分为相等大小的一个个区域,这个小的区域大小是512 Byte,称为Card。并维护了一个字节数组Card Table,Card Table的数组下标映射着每一个Card,一个Card的内存中通常包含不止一个对象,只要Card内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,称为这个元素变脏(Dirty),没有则标识为0。

(2)每个Region初始化时,会初始化一个remembered set

(3)RSet里面记录了引用——其他Region中指向本Region中所有对象的所有引用

(4)RSet其实是一个Hash Table,Key是其他的Region的起始地址,Value是一个集合,里面的元素是Card Table 数组中的index,既Card对应的Index,映射到对象的Card地址。

引用关系的记录方式通常有两种方式:「我引用了谁」和「谁引用了我」,前一种记录简单,但是在回收时需要对记录集做全部扫描,后一种记录复制,占用空间大,但是在回收时只需要关注对象本身,即可通过 RSet 直接定位到引用关系。G1 的 RSet 使用的是后一种「谁引用了我」的记录方式,其数据结构可本质上是一个哈希表。每次向引用类型字段赋值时,会触发:「写屏障 -> 线程队列 -> 全局队列 -> 并发 RSet 更新」这样一个过程。在垃圾收集时,无需遍历所有的Region,只需要筛选出进行垃圾收集的Region的卡表中变脏的元素,就能轻易得出Card卡页内存块中包含跨代指针。

另一个问题就是何时更新RSet,G1会采用post-write barrier(写后屏障)来完成RSet的更新,即引用字段赋值后同时通过JVM的post-write barrier机制完成卡表状态的更新。G1 中的写屏障分为 pre_write_barrier 和 post_write_barrier,其中 SATB(后文会讲)机制使用了pre_write_barrier ,RSet使用了post_write_barrier。如下面的代码所示,应用 field 将要被赋予新值 value,由于 field 指向的旧的引用对象会丢失引用关系,因此在赋值之前会触发 pre_write_barrier,更新 SATB 日志记录,记录下引用关系变化时旧的引用值;在正式赋值之后,会执行 post_write_barrier,更新新引用对象所在的RSet,即下面代码的步骤3。

// 赋值操作,将 value 赋值给 field 所在的引用
void assign_new_value(oop* field, oop value) {  
  pre_write_barrier(field);         // 步骤1:更新 SATB 日志记录
  *field = value;                   // 步骤2:引用赋值
  post_write_barrier(field, value); // 步骤3:更新引用对象所在的RSet
}

1.4 垃圾的标记机制

G1垃圾处理器设计的一大原则就是尽可能缩短STW的时间,因此在一些步骤上会放弃使用STW并采取其他措施来保证垃圾收集能够快速且正常地工作。G1垃圾处理器在寻找“GC Roots”节点时是使用了STW的(目前为止所有的垃圾处理器在这一步都是使用STW的),而根据“GC Roots”节点进行可达性分析和标记的时候却没有使用STW,而是允许用户线程与垃圾收集线程并发执行的,能够执行这一优化的背后就是G1使用了原始快照机制(Snapshot At The Beginning,SATB)。

1.4.1 三色标记法

G1采用三色标记的机制来进行可达性分析和标记对象,三色标记法将对象分成三种类型: (1)黑色:该对象是根对象,或者该对象与它的子对象都被扫描过(对象被标记了,且它的所有引用类型字段也被标记完了)。 (2)灰色:该对象本身被扫描了,但还没扫描完该对象中的子对象(它的引用类型字段还没有被标记完成)。 (3)白色:该对象未被扫描。完成所有对象的扫描之后,仍然被标记为白色的对象即为垃圾对象(对象没有被标记到)。

顺着之前的说法,G1在进行垃圾对象标记的时候没有使用STW,那么就会存在1.2.1小节中可达性分析算法的不足里面所提到的问题:垃圾收集进程将对象标记好颜色后,用户线程又修改了引用关系,这样可能出现把原本存活的对象错误标记为已消亡的情况,这就是非常致命的后果了,程序肯定会因此发生错误。具体如下所示:

图3:并发标记错误

如果在垃圾标记过程中,用户线程修改了对象的引用,例如图3中的一个正在扫描的灰色对象C的一个指向白色对象D的引用被用户线程切断了,而且用户还将黑色对象A的一个引用指向了白色对象D,那么就只有一个黑色对象指向白色对象D,由于黑色对象的引用不会被扫描,因此这白色对象D会被当成垃圾处理掉,但是它本不应该被处理的,这样就会出现程序问题。

1.4.2 增量更新和原始快照

Wilson于1994年在理论上证明了,当且仅当以下两个条件同时满足时,会产生图3所示的并发标记错误问题: (1)赋值操作插入了一条或多条从黑色对象到白色对象的新引用; (2)赋值操作删除了全部从灰色对象到该白色对象的直接或间接引用。

由于两个条件同时满足才会产生上述问题,因此要避免此类问题只需破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。 增量更新(Incremental Update)要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了,因此不存在一个赋值操作会插入一条或多条从黑色对象到白色对象的新引用。CMS垃圾处理器采用的是增量更新的机制。 原始快照(Snapshot At The Beginning,SATB)要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,因此从逻辑上来说赋值操作无法删除灰色对象到白色对象直接引用关系。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照来进行搜索。G1垃圾收集器采取的是原始快照SATB的机制来防止并发错误。

SATB 的过程可以简单理解为:当并发标记阶段引用的关系发生变化时,旧引用所指向的对象就会被标记,同时其子引用对象也会被递归标记,这样快照的完整性就得到保证了。SATB 的记录更新是由 pre_write_barrier 写屏障触发的,下面是 G1 论文中介绍的 SATB 原始表述,具体实现时,还是由两级的队列结构缓存,再由并发标记线程批量处理进入标记队列satb_mark_queue。

void pre_write_barrier(oop* field) {  
  oop old_value = *field;  
  if (old_value != null) {  
    if ($gc_phase == GC_CONCURRENT_MARK) {
      $current_thread->satb_mark_queue->enqueue(old_value);  
    }  
  }  
}

因此,G1 在结束并发标记后还有一个需要使用了STW 的最终标记(Final Marking)阶段就可以理解了,因为如果不引入一个采用了STW的最终标记(Final Marking)的过程,那么新的引用变更会不断产生,永远就无法达成完成标记的条件。最终标记标记阶段,因为有了SATB 的设计,则只需要扫描 satb_mark_queue 队列里的引用变更记录就可以对此次 GC 活动形成完整标记了(可以对比 CMS 的 remark 阶段)。

1.5 可预测的停顿时间

G1垃圾收集器的另外一个核心特色是可控制的停顿时间。为什么G1能做到这一点呢?G1回收是选择一些Region进行回收,而不是整代内存来回收,这是G1跟其它GC非常不同的一点。其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点。在G1的参数中,用户可以通过指定-XX:MaxGCPauseMillis参数来指定G1的停顿时间。

G1收集器要怎么做才能预测并控制停顿时间呢?G1的停顿时间的预测模型是以衰减均值(Decaying Average)为理论基础的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的时间成本,并分析得出平均值、标准偏差、置信度等统计信息,同时再通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过用户设置的期望停顿时间的约束下获得最高的收益,以此来规划出满足用户指定停顿时间的垃圾收集步骤。

2. G1的运作过程

G1的GC操作可以分为三种:Young GC,并发标记周期(Old GC)和Mixed GC。

2.1 G1 Young GC

与其他收集器的新生代gc类似,G1的Young GC也是采用标记-复制-清除算法。G1的Young GC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC;直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

2.2 G1 并发标记周期

G1收集器的并发标记周期大致可划分为以下四个步骤:

初始标记(Initial Marking,需要使用STW):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要Stop the world停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。

并发标记(Concurrent Marking,无需STW):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行(这也是G1的效率较高的原因)。该阶段不用stop the world,因此会使用三色标记法以及SATB机制来保证标记的正确性。

最终标记(Final Marking,需要使用STW):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。在该阶段会处理在并发标记阶段被用户修改过引用关系的对象,即SATB机制的二次标记。

筛选回收(Live Data Counting and Evacuation,需要使用STW):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间(这个是标准的垃圾收集算法中的标记-复制算法)。这里的操作涉及存活对象的移动,是必须暂停用户线程的,而且是由多条收集器线程并行完成的。

从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的(stop the world),换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。

2.3 G1 Mixed GC

Mixed GC是指目标为收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。成功完成并发标记周期后, 若是老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发Mixed GC流程,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,Mixed GC过程主要使用复制算法,把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。

补充:在垃圾收集和处理过程中,还有几种情况下会触发Full GC:

(1)并发模式失效

G1启动并发标记周期,但是在混合gc之前,老年代就被填满了,这时候G1就会放弃标记周期,改为执行Full gc,对应的gc日志为:[GC concurrent-mark-abort]

解决办法:发生这种失败意味着堆的大小应该增加了,或者G1收集器的后台处理应该更早开始,或者需要调整周期,让它运行得更快(如增加后台处理的线程数)。

(2)晋升失败

G1在进行新生代gc时,老年代没有足够的内存提供给晋升对象,将会触发Full gc。对应的gc日志为:to-space exhausted。解决这种问题的方式是:

a. 增加 -XX:G1ReservePercent 选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量。

b. 通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。

c. 也可以通过增加 -XX:ConcGCThreads 选项的值来增加并行标记线程的数目。

(3) 疏散失败

进行新生代gc时,survivor和老年代没有足够的空间容纳存活的对象。对应的gc日志为: to-space overflow。解决办法与晋升失败的情况是一样的。

(4) 巨型对象分配失败

巨型对象分配失败也会触发Full gc,解决办法:增大regionSize。

(5)metaspace gc

metaspace大小达到阈值(metaspaceSize大小,是动态的),会触发Full gc。

3. G1的调优参数

-XX:+UseG1GC:使用G1收集器

-XX:ParallelGCThreads:指定并行工作的GC线程数,也就是在STW阶段工作的GC线程数。

-XX:ConcGCThreads:指定并发工作的GC线程数,默认是-XX:ParallelGCThreads的四分之一,也就是在非STW期间的GC工作线程数,当然其他的线程很多工作在应用上。当并发周期时间过长时,可以尝试调大GC工作线程数,但是这也意味着此期间应用所占的线程数减少,会对吞吐量有一定影响。

-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区。Region的大小主要是关系到Humongous Object的判定,当一个对象超过Region大小的一半时,则为巨型对象,那么其会至少独占一个Region,如果一个放不下,会占用连续的多个Region。

-XX:MaxGCPauseMillis:目标暂停时间(默认200ms),表示每次GC最大的停顿毫秒数,默认值是200ms,VM将调整Java堆大小和其他与GC相关的参数,以使GC引起的暂停时间短于预设值。这个值不能设置的过小,如果设置过小则每一次垃圾处理所能选择的Region区域会减少,这会导致GC次数的增加,可能最后GC的垃圾清理速度赶不上应用产生的速度,那么可能会造成串行的Full GC,这是要极力避免的。所以暂停时间肯定不是设置的越小越好,当然也不能设置的偏大,转而指望G1自己会尽快的处理,这样可能会导致一次全部并发标记后触发的Mixed GC次数变少,但每次的时间变长,STW时间变长,对应用的影响更加明显。

-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent,分别为新生代比例的设定数值的下限和上限,默认值分别为5%和60%。G1会根据实际的GC情况(主要是暂停时间)来动态的调整新生代的大小,主要是调整Eden Region的个数。

-XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代,默认值是15。一般新生对象经过15次Young GC会晋升到老年代,巨型对象会直接分配在老年代,同时在Young GC时,如果相同age的对象占Survivors空间的比例超过 -XX:TargetSurvivorRatio的值(默认50%),则会自动将此次晋升年龄阈值设置为此age的值,所有年龄超过此值的对象都会被晋升到老年代,此举可能会导致老年代需要不少空间应对此种晋升。一般这个值不需要额外调整。

-XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),如果Mixed GC周期结束后老年代使用率还是超过InitiatingHeapOccupancyPercent值,那么会再次触发全局并发标记过程,这样就会导致频繁的老年代GC,影响应用吞吐量。同时老年代空间不大,Mixed GC回收的空间肯定是偏少的。可以适当调高该值,当然如果该值太高,很容易导致年轻代晋升失败而出发Full GC,所以需要多次调整测试。

-XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。

补充:虽然G1有不少优秀的特性,但是G1在垃圾收集时的内存占用和程序额外负载都比CMS要高,因此具体用什么垃圾收集器还是要从各方面考虑。一般来说,小内存应用上,CMS会比G1更占用;而在大内存的服务器上(6G以上的内存),G1垃圾收集器能够发挥出更大的优势。

转自:https://zhuanlan.zhihu.com/p/519446431


标题:G1垃圾收集器原理
作者:michael
地址:https://blog.junxworks.cn/articles/2023/11/08/1699419386323.html