(一)万字详解G1垃圾收集器 —G1的设计目标是什么?G1的分区是什么?卡表的作用和工作原理?如何解决漏标问题?
一、G1垃圾收集器简介
G1 GC(Garbage-First Garbage Collector)是一款先进的垃圾收集器,通过 -XX:+UseG1GC
参数启用。它首次亮相于JDK 6u14版本,并在JDK 7u4中正式发布。对于熟悉JVM的开发者而言,G1已是一个广为人知的名字。值得注意的是,在JDK 9中,G1被提议成为默认的垃圾收集器(JEP 248)。
G1是专为服务器端应用设计的垃圾收集器,特别适用于多核处理器和大内存环境。它的独特之处在于能够在保证高吞吐量的同时,尽可能地满足用户对垃圾收集暂停时间的要求。
G1的设计目标
G1的设计初衷主要针对以下应用场景:
- 与应用程序线程并发执行,类似于CMS收集器。
- 更高效地整理空闲内存空间。
- 提供更可预测的GC停顿时间。
- 在不过度牺牲吞吐性能的前提下优化垃圾收集。
- 无需增加Java堆内存即可有效工作。
G1相比CMS的优势
G1收集器的目标是取代CMS收集器,相较于CMS,G1在以下方面表现更为出色:
- G1具备内存整理功能,能有效减少内存碎片。
- G1的"Stop The World"(STW)时间更易于控制。
- G1引入了停顿时间预测机制,允许用户指定期望的停顿时间,从而提供更好的服务质量
二、G1中几个重要概念
1. 分区(Region)
G1收集器引入了分区(Region)的概念,这是它区别于传统GC收集器的关键特征之一。
传统GC vs G1 内存划分
传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace)。这种划分的特点是各代的存储地址(逻辑地址)是连续的。如下图所示:
相比之下,G1的内存划分更加灵活和动态:
Region的特性
- 数量和大小:
- 默认情况下,G1最多可以有2048个Region。
- 每个Region的大小通常是堆内存大小除以2048。
- 可以通过
-XX:G1HeapRegionSize
参数手动指定Region大小(必须是2的幂,范围在1MB到32MB之间)。
- 存储地址:
- G1中各代的存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region。
- 每个Region占有一块连续的虚拟内存地址。
- Region可以根据需要在年轻代和老年代之间动态切换。
- 数量与大小关系:
- 堆内存总量固定时,Region数量与大小成反比。例如,4GB堆可以有2048个2MB的Region,或1024个4MB的Region。
Region数量较多的优势
- 更好的内存利用率:小对象可以更紧密地填充在Region中,减少内存碎片。
- 更灵活的GC管理:G1可以更精确地选择需要回收的Region,控制GC范围和时机。
- 更好的负载平衡:多核环境下,GC线程可以更均匀地分配到各个Region上。
- 更容易处理大对象:即使是大对象也只占用相对较少的Region数量,便于处理。
- 更可预测的停顿时间:G1可以更精确地控制每次GC的停顿时间,满足用户设置的期望停顿时间。
初始Region分配策略
- 年轻代与老年代的划分:
- G1保留了年轻代和老年代的逻辑概念,但物理上不再固定隔离。
- 默认年轻代占整个堆内存的5%。
- 可通过
-XX:G1NewSizePercent
和-XX:G1MaxNewSizePercent
参数调整。
- 动态调整:
- G1会根据应用的内存使用情况和垃圾收集效果动态调整Region分配。
- 例如,如果年轻代对象频繁晋升到老年代,G1可能会增加老年代的Region数量。
通过这种灵活的分区机制,G1能够更好地适应不同应用的内存需求,提高垃圾收集效率和整体性能。
2. 巨型对象(Humongous Region)
2.1 巨型对象的定义
巨型对象(Humongous Object)是指大小达到或超过分区(Region)大小一半的对象。G1垃圾收集器对这类对象有特殊的处理机制。
2.2 巨型对象的分配策略
与传统的垃圾收集器不同,G1并不会将巨型对象直接分配到老年代。相反,G1为巨型对象专门设计了Humongous区。这种设计有以下优点:
- 节约老年代空间
- 避免因老年代空间不足而引发的GC开销
2.3 巨型对象的回收机制
巨型对象的回收与新生代和老年代的回收同步进行。在G1进行新生代或老年代回收时,会顺带对大对象进行回收。这种策略有效地整合了大对象的生命周期管理。
G1在处理巨型对象时,会尽量避免进行拷贝操作,因为巨型对象的拷贝成本较高。在回收过程中,G1会优先考虑回收没有巨型对象的Region,以提高回收效率并减少拷贝成本。
2.4 巨型对象的存储策略
当一个Humongous Region无法容纳一个巨型对象时,G1会寻找连续的Humongous Region来存储。虽然G1在大多数情况下将Humongous Region视为老年代的一部分,但它们的处理方式仍有所不同。
2.5 减少巨型对象影响的策略
为了减少连续Humongous对象分配对GC的影响,可以采取以下策略:
- 将大对象转化为普通对象
- 增大Region大小(前提是已知存在大对象,否则建议使用默认设置)
Region大小可以通过-XX:G1HeapRegionSize
参数设定:
- 取值范围:1MB到32MB
- 必须是2的幂
- 默认情况:G1将整个堆划分为2048个分区
如果不手动设定,G1会根据堆大小自动决定Region大小。通过合理设置Region大小,可以有效管理巨型对象,优化G1的性能。
3. 卡表(Card Table)
3.1 卡表概述
卡表(Card Table)是G1垃圾收集器中的一个重要数据结构。它是由元素为1字节的数组实现的,数组中的每个元素称为卡片或卡页。卡表映射到整个堆空间,每个卡片对应堆中的512字节空间。
具体来说:
- 每个Region被分成若干个大小为512字节的连续内存区域。
- 以1MB大小的Region为例,每个Region对应2048个Card Page。
- Card Table是一种points-out("我引用了谁的对象")的结构。
- Card Table用单字节的信息映射一个Card。
- 当Card中存储了对象时,这个Card被称为脏卡(dirty card)。
- 一些频繁访问的Card会被存放到Hot card cache中。
可以通过以下公式计算对象所在的Card位置索引:
(对象的地址 - 堆开始的地址) / 512
3.2 Card Table的作用
Card Table的主要作用是跟踪整个堆内存中各个卡(Card)的状态,以优化垃圾收集(GC)过程。它的设计初衷是为了解决分代收集时,live set(存活对象集合)扫描需要在不同代之间穿梭时的效率问题。
3.3 Card Table的工作原理
-
存储位置:Card Table存储在JVM的堆内存中,是一个全局的数据结构。
-
卡的划分:每个Region被分成多个512字节的连续内存区域(卡)。每个卡对应Card Table中的一个元素。
-
状态记录:当堆内存中的对象引用发生变化时,JVM通过写屏障(Write Barrier)机制标记该对象所在的卡为"脏"(Dirty)。
-
GC处理:在垃圾收集时,收集器会检查Card Table中标记为Dirty的卡,将这些卡中的对象视为潜在的可达对象,进行进一步扫描。
3.4 维护机制
Card Table的维护主要依赖于写屏障(Write Barrier)技术:
-
写屏障是在对象引用赋值操作前后附加的一段代码,用于检测并更新卡表信息。
-
当新生代中的对象被老年代对象引用时,写屏障会检测这一变化,并在卡表中标记对应的卡。
-
当老年代对象更新对新生代的引用时,写屏障也会更新卡表,确保记忆集的准确性。
通过这种机制,Card Table能够有效地跟踪跨代引用,提高垃圾收集的效率。
4. SATB(Snapshot At The Beginning)写屏障技术的应用
SATB(Snapshot At The Beginning)写屏障是一种特殊的写屏障技术,主要用于解决并发标记算法中的漏标问题。
4.1 工作原理
- 在GC开始时(即并发标记的初始阶段),SATB记录所有GC Roots的初始状态(即"快照")。
- 在并发标记过程中,如果对象的引用发生变化(如新增引用或删除引用),SATB会将这些变化记录下来,但不会立即更新对象的标记状态。
- 在并发标记结束后,SATB根据记录的变化信息,重新遍历那些引用发生变化的对象,并根据新的引用关系更新它们的标记状态。
4.2 优点
- SATB能够确保在并发标记过程中,即使对象的引用发生变化,也不会导致漏标问题。
- 通过延迟更新对象的标记状态,减少了并发标记过程中的同步开销。
- 与RSet高效配合:G1中的每个region都包含一个hash表(RSet),用于记录指向当前region的外部引用。SATB算法能够很好地利用这一特性,因为它只需要读取RSet中的内容来更新对象的标记状态,而无需进行全堆扫描。这种高效的配合使得SATB算法在G1中更加适用。
5. 并发标记算法(三色标记法)
CMS和G1在并发标记时使用的是同一个算法:三色标记法,使用白灰黑三种颜色标记对象。
- 白色:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。
- 灰色:对象被标记了,但是它的field还没有被标记或标记完。
- 黑色:对象被标记了,且它的所有field也被标记完了。
GC开始前所有对象都是白色。GC一开始,所有根能够直达的对象被压到栈中,待搜索,此时颜色是灰色。然后灰色对象依次从栈中取出搜索子对象,子对象也会被涂为灰色,入栈。当其所有的子对象都涂为灰色之后该对象被涂为黑色。当GC结束之后灰色对象将全部没了,剩下黑色的为存活对象,白色的为垃圾。
5.1 漏标问题
在remark过程中,如果黑色对象指向了白色对象,且不对黑色重新扫描,则会出现漏标。这可能导致白色对象被错误地当作没有新引用指向而被回收。
产生漏标问题的条件有两个:
- 黑色对象指向了白色对象
- 灰色对象指向白色对象的引用消失
5.2 解决漏标
有两种主要方法来解决漏标问题:
- 增量更新(Incremental Update) :关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。CMS采用该方法。
- SATB(Snapshot At The Beginning) :关注引用的删除,当灰色对象到白色对象的引用消失时,将这个引用推到GC的堆栈,保证白色对象还能被GC扫描到。G1采用该方法。
5.3 CMS和G1的具体实现
-
CMS(增量更新): 在并发标记过程中,如果对象的引用发生变化,CMS会将这些变化记录下来,但不会立即更新对象的标记状态。在并发标记结束后,CMS会根据记录的变化信息,重新遍历那些引用发生变化的对象,并更新它们的标记状态。
-
G1(SATB):
- 引用删除的处理:当灰色对象删除了对白色对象的引用时,G1通过前置写屏障(Pre-write Barrier)来捕获这一变化,并将旧引用推送到特殊队列(如SATBMarkQueue)中。
- 保证白色对象被扫描:在并发标记的后期阶段,G1会处理SATBMarkQueue中的对象,以这些白色对象为根重新扫描它们的引用链,确保它们被正确地标记为存活或垃圾。
5.4 G1选择SATB的原因
G1采用SATB而不用增量更新的主要原因包括:
- 效率提升:SATB算法在开始时生成快照,减少了并发标记过程中需要处理的对象数量。
- 减少深度扫描:SATB不需要在重新标记阶段再次深度扫描被删除的引用对象,这对于G1的多区域结构更为有利。
- 可控的开销:尽管SATB可能产生更多浮动垃圾,但由于G1扫描固定个数的region,这种开销是可控的。
SATB的主要缺点是可能产生更多的浮动垃圾,但这些垃圾会在下次垃圾收集时被回收,不会对程序造成致命影响。相比之下,漏标的后果更为严重,因为它会直接影响程序的正确性。
6. 已记忆集合 Remember Set (RSet)
RSet,全称 Remembered Set,是一种记录跨代(或跨Region)引用的数据结构。在JVM中,RSet用于记录某个Region中的对象被其他Region中的对象所引用的信息。它属于points-into结构(谁引用了我的对象)。例如,Region2的RSet中记录着Region1和Region3都引用了它的对象。
6.1 RSet的作用
RSet的主要作用是通过快速定位可能包含跨代引用的对象,减少在GC过程中需要扫描的内存范围,从而提高GC的效率。
6.2 RSet的工作原理
-
存储位置:RSet主要存储在JVM的堆内存中。在G1垃圾收集器等现代JVM实现中,每个Region都有一个对应的RSet,用于记录其他Region对当前Region的引用关系。
-
数据结构:RSet通常实现为一个Hash Table。其中Key是引用当前Region中对象的外部Region的起始地址,Value是一个集合,包含了这些外部Region中引用当前Region内对象的Card Table的索引。
-
跨代引用处理:在GC过程中,当需要回收某个Region时,垃圾收集器会先检查该Region的RSet。如果RSet不为空,则意味着有其他Region中的对象引用了该Region中的对象。此时,垃圾收集器会根据RSet中的信息,只扫描那些可能包含跨代引用的外部Region中的Card Table,而不是扫描整个堆内存。
6.3 RSet的存储方式
RSet有三种主要的存储方式,根据引用关系的密集程度选择:
- 稀疏表(Sparse):
- 使用哈希表存储
- Key是region的索引(index),Value是card数组(card index)
- 适用于引用关系较为稀疏的情况
- 细粒度(Fine):
- 当某个Region的card数量超过一定阈值时使用
- 使用bitmap,每一位对应一个card的索引
- 适用于引用关系较为密集的情况
- 粗粒度(Coarse):
- 当被引用的Region数量超过一定阈值时使用
- 使用bitmap存储,每个分区对应一个比特位
- 只记录引用数量,需要通过整堆扫描找出所有引用
- 扫描速度最慢,但空间效率高
- 适用于引用关系非常密集,且对空间效率有较高要求的情况
7. 收集集合:Collection Set (CSet)
Collection Set是指在垃圾收集过程中被回收的Region集合,也可以理解为是垃圾收集暂停过程中被回收的目标。
- 这些Region可能包括年轻代(Young Generation)的Eden区、Survivor区,以及老年代(Old Generation)的某些Region。
- G1收集器会根据Region中的垃圾数量和回收收益来决定哪些Region应该被加入CSet进行回收。
7.1 CSet的生成与回收过程
-
在垃圾收集开始时,G1收集器根据一定的策略(如垃圾数量、回收收益等)选择一部分Region组成CSet。
-
G1收集器暂停应用程序的执行(Stop-The-World),并对CSet中的Region进行垃圾收集。
-
收集过程中,G1采用复制算法(对于年轻代)或标记-整理算法(对于老年代)来回收垃圾对象,并将存活的对象移动到其他Region中。
-
收集完成后,CSet中的Region被释放并加入到空闲Region队列中,供后续对象分配使用。
三、G1垃圾回收过程
G1提供了两种主要的垃圾收集模式:Young GC和Mixed GC。Full GC在G1中不是直接提供的,但在特定情况下(如Mixed GC无法跟上内存分配速度)会回退到串行老年代收集器(Serial Old GC)进行全堆扫描。
与CMS的"标记-清理"算法不同,G1从整体来看是基于"标记整理"算法实现的收集器;从局部上来看是基于"复制"算法实现的。
G1垃圾回收过程包含两个主要阶段:
- Young-only phase(年轻代专属阶段):
- 只回收年轻代
- 主要完成年轻代回收(Young GC)和老年代并发标记周期(Concurrent Marking Cycle)
- Space-reclamation phase(空间回收阶段):
- 进行多次年轻代收集(Young GC)以及增量回收部分老年代
- 被称为混合收集(Mixed GC)
- 当G1判断继续回收老年代不足以释放更多空间,或停顿时间大于MaxGCPauseMillis(默认200ms)时,会退出该阶段
1. Young GC(年轻代收集)
Young GC使用复制算法进行垃圾回收。它的触发并不仅仅依赖于Eden区是否已满,而是基于以下策略:
- G1会计算当前Eden区回收所需的时间
- 如果回收时间远小于-XX:MaxGCPauseMills参数设定的值,G1会增加年轻代的region,继续为新对象分配空间
- 当Eden区再次填满时,如果G1计算的回收时间接近-XX:MaxGCPauseMills设定的值,才会触发Young GC
1.1 触发条件
当年轻代的Eden区空间耗尽,无法为新创建的对象分配内存时,会触发G1 Young GC。
1.2 准备阶段
- 选择收集集合(CSet):
- G1在遵循用户设置的GC暂停时间上限的基础上,选择最大年轻代区域数
- 将这个数量的所有年轻代区域(Eden区和Survivor区)作为收集集合(Collection Set,简称CSet)
- 暂停应用程序(Stop-The-World):
- G1暂停所有应用程序线程
- 确保在根扫描过程中不会有新的对象被创建或引用关系发生变化
1.3 回收过程
a. 根扫描
- 从GC Roots开始遍历:
- G1垃圾收集器从GC Roots开始遍历,查找所有从根可达的对象
- 这些对象被标记为存活对象,在下一次垃圾回收中不应被回收
- 利用RSet(Remembered Set):
- 将RSet中记录的外部引用作为扫描存活对象的入口
- RSet用于跟踪其他区域(如老年代)对年轻代区域的引用
- G1同时考虑GC Roots和RSet中的引用关系,确保所有存活的对象都被正确标记
b. GC Roots
GC Roots是垃圾收集器特别关注的根对象集合,主要包括:
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中的类静态属性引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
c. 更新RSet
- 脏卡表处理:
- 在更新RSet之前,G1先处理脏卡表(Dirty Card)
- 脏卡表记录了自上次GC以来被修改过的内存区域(可能有新的跨代引用产生)
- 更新RSet:
- 通过遍历脏卡表,G1找到所有被老年代引用的年轻代对象
- 将这些引用关系更新到RSet中
- 在后续的根扫描过程中,G1利用RSet中的信息避免误回收这些对象
d. 移动存活对象
-
遍历标记栈,将栈内的所有存活对象移动至Survivor区域或老年代区域(如果对象年龄达到晋升阈值)
-
如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间
-
Survivor区的数据也会移动到新的Survivor区中,部分数据可能晋升到老年代
2. Mixed GC(混合收集)
Mixed GC在堆占有率达到设定值时触发(由-XX:InitiatingHeapOccupancyPercent参数控制,默认值是45%)。它会回收所有的Young区、部分Old区(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区。
在Mixed GC过程中,需要将各个region中存活的对象拷贝到别的region里。如果发现没有足够的空region能够承载拷贝对象,就会触发一次Full GC。
相关参数:
- -XX:G1MixedGCCountTarget:设置在一次混合回收的过程中,最后一个阶段执行几次混合回收,默认值是8次。
- -XX:G1HeapWastePercent:默认值是5%,表示当混合回收时,一旦空闲出来的Region数量达到了堆内存的5%,就会立即停止混合回收。
2.1 全局并发标记
a. 初始标记(同CMS)
初始标记阶段会Stop The World(STW),通常耗时很短。它是伴随Young GC同步完成的,主要完成两件事:
- 标记GC Roots直接关联的对象
- 标记出所有的survivor区(Root区)
初始标记搭载Young GC的原因:
- 减少停顿时间:利用Young GC的STW时间,同时完成初始标记工作。
- 提升效率:Young GC和初始标记都涉及处理年轻代,可以复用这个过程,提高垃圾收集效率。
b. 并发标记(同CMS)
并发标记阶段从GC Roots的直接关联对象开始遍历整个对象集合。这个过程耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行。主要完成三件事:
- 从GC Root开始,对堆中所有对象进行可达性分析,确认需要回收的对象
- 更新卡表
- 标记空的Region
c. 重新标记(Remark)
由于并发标记阶段应用线程还在运行,可能会产生新的对象或改变对象的引用关系。因此G1需要再次进行短暂的STW,以标记那些在并发标记阶段发生变化的对象。重新标记主要完成两件事:
- 回收并发标记过程中的空Region
- 利用起始快照算法(Snapshot-At-The-Beginning,SATB)修正并发标记中的数据
2.2 移动/拷贝存活对象
-
在混合回收阶段,G1根据并发标记的结果,选择部分老年代区域和整个年轻代区域进行回收。这个阶段会停止应用线程的执行,进入STW状态。
-
G1遍历选定的区域,将存活的对象复制到新的区域中。如果是年轻代,则复制到Survivor区或老年代;如果是老年代,则复制到其他空闲的老年代区域。
-
复制完成后,G1清理掉原区域中的垃圾对象,并释放相应的内存空间。
a. 选择收集集合(CSet)
根据全局并发标记的结果和停顿时间目标,选择部分老年代区域和整个年轻代区域作为CSet。
b. 清理阶段-筛选回收(Cleanup)
- 对存活对象进行统计并完全释放空闲区域。(STW)
- 清理记忆集(Remembered Sets)。(STW)
- 重置空闲区域并将它们返回到空闲列表。(并发执行)
筛选回收的实现:
- G1对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间(通过-XX:MaxGCPauseMillis参数指定)来制定回收计划。
- G1在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。
2.3 整理内存空间
由于混合回收涉及到了老年代的回收,G1还会对内存空间进行整理,以减少内存碎片的产生。这个过程类似于标记-整理算法中的整理阶段。
注意:G1混合回收的算法结合了复制算法和整理算法的优点。在年轻代回收时,G1更倾向于使用复制算法来减少GC停顿时间;而在老年代回收时,G1则会使用标记-整理算法来整合内存碎片并释放更多的内存空间。
3. Full GC(全堆收集)
Full GC是G1最后的防护线,在设计时需要尽量避免。在G1中,Full GC通常不是由G1直接触发的,而是在特定情况下(如Mixed GC无法跟上内存分配速度)会回退到串行老年代收集器(Serial Old GC)进行全堆扫描。Full GC是一个耗时的过程,会停止所有应用线程,直到垃圾收集完成。
G1主要通过以下几个参数和指标来决定是否需要触发Full GC:
参数 | 默认值 | 说明 | 举例 |
---|---|---|---|
-XX:G1HeapWastePercent | 5% | 堆中可以容忍的最大垃圾比例。如果在Mixed GC之后,垃圾的比例超过了这个阈值,G1可能会触发Full GC来回收更多的空间 | -XX:G1HeapWastePercent=10 意味着G1将在预计回收超过堆的10%时启动混合垃圾收集。 |
-XX:G1MixedGCLiveThresholdPercent | 85% | 当Old区中的对象占用的比例超过多少时,这部分区域会被包含在Mixed GC中,默认85。如果这个比例设置得太低,可能会导致过多的Old区域被包含在Mixed GC中,进而增加GC的工作量和停顿时间,最终可能引发Full GC | -XX:G1MixedGCLiveThresholdPercent=60 表示如果混合垃圾收集后旧区中的存活对象比例低于60%,G1将尝试在下一次混合垃圾收集中回收更多区域。 |
-XX:G1MixedGCCountTarget | 8 | 在开始进行Full GC之前,可以执行的Mixed GC的最大次数。如果连续的Mixed GC没有有效地回收内存,达到这个次数限制后,G1可能会触发Full GC。 | -XX:G1MixedGCCountTarget=4 表示G1将尝试在并发标记周期结束时,通过执行至多4次混合垃圾收集来清理足够的空间。 |
-XX:G1ReservePercent | 10% | 保留的堆内存的百分比,默认是10,作为一个缓冲区来减少Full GC的发生。如果可用内存低于这个阈值,G1可能会触发Full GC | -XX:G1ReservePercent=10 表示JVM堆中将保留10%的空间作为预留空间,用于在并发垃圾收集期间使用。 |
这些参数可以根据具体应用的需求进行调整,以优化G1的性能并尽可能避免Full GC的发生。
三、G1和CMS差异
1. 相同点
-
目标:两者都旨在通过回收JVM堆内存中的无用对象来优化内存使用,并减少内存泄漏的风险。
-
Full GC:在特定条件下,G1和CMS都会触发Full GC,即对整个堆(包括年轻代和老年代)进行垃圾收集。
2. 区别
特性 | G1 | CMS |
---|---|---|
适用场景 | 适用于大内存、多CPU的服务端应用 | 适用于对停顿时间要求较高的应用,如互联网网站或B/S系统的服务端 |
垃圾收集算法 | 基于区域的内存管理,将堆划分为多个Region,采用标记-整理算法,局部(Region之间)可能采用复制算法 | 基于标记-清除算法,仅作用于老年代 |
Full GC时的停顿 | 在进行Full GC时需要暂停用户线程 | 在Full GC时尽量不暂停用户线程,但初始标记和重新标记阶段会STW |
内存碎片 | 通过整理内存区域,减少了内存碎片 | 由于采用标记-清除算法,可能会产生内存碎片 |
内存压缩 | 支持在垃圾收集时进行内存整理和压缩 | 不支持内存压缩,特定条件下支持压缩(UseCMSCompactAtFullCollection设置为true时),CMS支持在Full GC过程中进行内存压缩整理。通过合理设置CMSFullGCsBeforeCompaction参数,用户可以在减少内存碎片与降低停顿时间之间找到最佳的平衡点。 |
CPU资源需求 | 需要更多的CPU资源来运行,以缩短STW时间 | 对CPU资源敏感,但总体需求相对较少 |
默认性 | 从JDK 9开始,G1成为默认的垃圾回收器 | 在早期JDK版本中,CMS是可选的并发垃圾收集器,但不是默认选项 |
3. 优缺点
1)G1 - 优点
a. 高效并行与并发,适用于大内存、多核环境 G1采用了并行和并发的方式进行垃圾收集,可以充分利用多核处理器的计算能力。在回收期间,多个GC线程可以同时工作,提高了垃圾收集的效率。G1的设计初衷就是针对拥有多核处理器和大内存的机器,通过并行和并发的方式提高垃圾收集的效率,同时减少停顿时间,满足服务端应用的需求。
b. 分代收集 G1虽然依然区分年轻代和老年代,但不再坚持固定大小和固定数量的堆区域划分。它将堆空间分为若干个区域(Region),这些区域在逻辑上可以是年轻代或老年代的一部分,回收的粒度更细,范围更小,使得垃圾收集更加灵活。
c. 空间整合与减少内存碎片 G1使用标记-整理算法,对内存进行压缩和整理,减少了内存碎片的产生。这种特性有利于程序长时间运行,尤其是在分配大对象时不会因为找不到连续的内存空间而提前触发GC。
d. 可预测的停顿时间 G1允许用户设定GC的停顿时间目标,通过跟踪各个Region的垃圾堆积价值,优先回收价值最大的Region,从而在保证吞吐量的同时,尽量满足用户设定的停顿时间要求。
2)G1 - 缺点
a. CPU资源消耗 G1在垃圾收集过程中需要多个GC线程同时工作,这会增加CPU的负载。尤其是在高负载情况下,可能会影响应用程序的性能。
b. 实现复杂度 G1引入了新的数据结构和算法(如RSet、Card Table等),使得实现相对复杂。这增加了维护的难度,也提高了出错的概率。
c. 在某些场景下吞吐量可能不如其他收集器 虽然G1在大多数情况下都能提供较好的性能,但在某些特定场景下(如小内存应用),其吞吐量可能不如其他收集器(如CMS,G1复杂的数据结构和算法会占用一定的性能;多线程并行执行由于小内存资源有限,多线程并行可能会引发线程资源竞争和上下文切换开销进而降低系统吞吐量)。
3)CMS - 优点
a. 低停顿时间 CMS采用了并发标记和并发清除的方式,大部分垃圾收集工作都可以与应用程序并发执行,从而减少了用户线程的停顿时间。
b. 高吞吐量,与应用程序并发执行 由于CMS在并发阶段不会暂停用户线程,因此可以保持较高的应用程序吞吐量,通过并发执行的方式实现这一目标。
4)CMS - 缺点
a. 内存碎片 CMS采用标记-清除算法,可能会导致内存碎片的产生。当内存碎片过多时,可能需要提前触发Full GC来整理内存,从而影响性能。
b. 对CPU资源敏感 CMS在并发标记和并发清除阶段会占用一部分CPU资源,这可能会导致应用程序的吞吐量下降。
c. 无法处理浮动垃圾,可能产生"Concurrent Mode Failure" 在并发清除阶段,用户线程还在运行,可能会产生新的垃圾对象(浮动垃圾)。这些垃圾对象需要在下一次GC时才能被清理,从而增加了GC的负担。当老年代内存不足以存放新产生的浮动垃圾时,CMS可能会触发"Concurrent Mode Failure",导致另一次Full GC的产生。这会增加停顿时间,并影响性能。
4. 什么场景适合G1
-
50%以上的堆被存活对象占用 使用G1,就不用特意预留出很大的老年代空间,G1会根据对象存活状态,动态分配每种不同代对象需要占用的空间。
-
对象分配和晋升的速度变化非常大 前提还是大内存机器才使用G1,大内存的主机如果对象分配和晋升的速度变化非常快的话,G1的这种内存设计可以很快的划分出对应所需的区域【区域占比动态增长,不像CMS等垃圾收集器要划分固定的空间来区分年轻代和老年代】,但因为G1算法比较复杂,在小内存机器里面性能不如CMS等主流垃圾收集器。
-
停顿时间自己掌控 G1有一大好处就是可以设置我们每次想要回收的停顿时间【-XX:MaxGCPauseMillis】,可以有效提升用户体验。
-
8GB以上的堆内存(建议值) G1适合8G以上内存的机器使用【结构设计,2048个Region,内存太小的话每个Region也很小,很容易就超过Region的一半被识别为超大对象,这样Humongous区东西会很多,反而不能很好的进行GC收集】。
感谢大家的观看!!!创作不易,如果觉得我写的好的话麻烦点点赞👍支持一下,谢谢!!!
转载自:https://juejin.cn/post/7419869992617377832