G1垃圾回收器专栏
(一)G1垃圾回收器
【1】简单介绍
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1垃圾回收器重新定义了堆空间,打破了原来的分代模型(堆分成一块新生代和一块老年代),而是把整个堆分成2048个大小相等的分区Region,这样垃圾回收器每次GC的时候就不用对整个堆进行垃圾回收,可以由用户指定收集操作在多长时间内完成,在预测的停顿时间内对部分区域进行垃圾回收。
高吞吐量
(1)区域划分特点:
(1)G1 不再坚持固定大小和固定数量的分代区域划分,而是把连续的 Java 堆划分为约2048个大小相等的分区(Region),每个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。
(2)Region 中还有一类特殊的 Humongous 区域,专门来存储大对象(大小超过一个 Region 容量的一半);而对于超过整个 Region 的超大对象,将会被存在 N 个连续的 Humongous Region 中(G1 的大多数行为都把 Humongous Region 作为老年代的一部分看待)。
(3)从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量。(一个region有可能属于Eden、Survivor、Old或者Humongous区域,但是一个region只可能属于一个角色)
(2)垃圾回收特点:
(1)并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop-the-world 停顿的时间,部分其他收集器原来需要停顿 Java 线程执行的 GC 操作,G1 收集器仍然可以通过并发的方式让 Java 程序继续运行。
(2)分代收集:打破了原有的分代模型,将堆划分为一个个区域。
(3)空间整合:与 CMS 的“标记-清除”算法不同,G1 从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着 G1 运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。
(4)可预测的停顿:这是 G1 相对于 CMS 的一个优势,降低停顿时间是 G1 和 CMS 共同的关注点。但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上的垃圾回收时间都在这个时限内。
(3)内存划分图
Region的归属不是固定的,在G1中,每一个Region都可能属于新生代,也可能属于老年代。
刚开始的时候,Region可能不属于新生代也不属于老年代,但是过段时间之后,被G1分配给了新生代,然后放了很多新生代的对象。在经历了垃圾回收之后,可能下一次同一个Region又被分配给了老年代,用来存放老年代的长生存周期的对象。
因此,在G1垃圾回收器中,没有给新生代和老年代分配多大内存的设定,他们各自的内存大小是在不停变动的,都是由G1控制。
(4)停顿时间模型(如何实现可预测时间)
停顿时间模型(Pause Prediction Model):指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过 N 毫秒。
G1 收集器之所以能建立可预测的停顿时间模型,是因为它将 Region 作为单次回收的最小单元(每次收集到的内存空间都是 Region 大小的整数倍),这样可以有计划地避免整个 Java 堆进行全区域垃圾收集。更具体的处理思路:让 G1 收集器去跟踪各个 Region 中的垃圾堆积的“价值”大小,然后在后台维护一个优先级列表,每次根据用户设定的收集停顿时间,优先处理回收价值收益最大的那些 Region
“价值”的衡量指标是:基于平均衰减的数学模型得出的每次回收所获得的空间大小以及回收所需时间的经验值。
G1通过追踪发现,现在新生代有两个Region,第一个Region存储了10M的垃圾对象,回收大约耗时1s,另外一个是20M,回收大概是200ms,然后再垃圾回收的时候,G1会发现在最近的一个时间段内,比如一小时内,垃圾回收已经导致几百毫秒的停顿了,现在又要执行一次垃圾回收,那么必须是回收上图中那个只要200ms的就能回收调20M的垃圾啊,于是G1出发垃圾回收,虽然可能导致200ms的停顿,但是一下回收了更多的垃圾。
所以G1可以做到让你来设定垃圾回收的系统的影响,G1通过把内存拆分成大量的Region,以及记录Region中可以回收对象的大小和时间,以至于再垃圾回收的时候,尽量在设定的时间内,尽可能多的回收更多的垃圾对象。
(5)G1收集器的应用场景
G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,尽可能的满足垃圾回收时的暂停时间。
就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:
(1)服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
(2)应用在运行过程中会产生大量内存碎片、需要经常压缩空间
(3)想要更可控、可预期的GC停顿周期,防止高并发下应用雪崩现象
(6)详细介绍
堆内存会被切分成为很多个固定大小的 Region,每个是连续范围的虚拟内存。堆内存中一个 Region 的大小可以通过-XX:G1HeapRegionSize参数指定,其区间最小为 1M、最大为 32M,默认把堆内存按照 2048 份均分。
每个 Region 被标记了 E、S、O 和 H,这些区域在逻辑上被映射为 Eden,Survivor 和老年代。存活的对象从一个区域转移(即复制或移动)到另一个区域,区域被设计为并行收集垃圾,可能会暂停所有应用线程。
如上图所示,区域可以分配到 Eden,Survivor 和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region)。Humongous 区域是为了那些存储超过 50% 标准 Region 大小的对象而设计的,它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象,那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区,有时候不得不启动 Full GC。
G1 收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 会通过一个合理的计算模型,计算出每个 Region 的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的 Region 作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。
对于打算从 CMS 或者 ParallelOld 收集器迁移过来的应用,按照官方的建议,如果发现符合如下特征,可以考虑更换成 G1 收集器以追求更佳性能:
(1)实时数据占用了超过半数的堆空间;
(2)对象分配率或“晋升”的速度变化明显;
(3)期望消除耗时较长的GC或停顿(超过 0.5 ~ 1 秒)。
【2】G1 的 GC 模式可以分为两种
(1)Young GC
在分配一般对象(非巨型对象)时,当所有 Eden 区域使用达到最大阀值并且无法申请足够内存时,会触发一次 YoungGC。每次 Young GC 会回收所有 Eden 以及 Survivor 区,并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。
(2)Mixed GC
G1 收集器之前的其他所有收集器(包括 CMS 收集器),垃圾收集的目标范围要么是整个新生代(Minor GC),或者整个老年代(Major GC),抑或整个 Java 堆(Full GC)。
而 G1 跳出了这个樊笼:它可以面向堆内存中任何部分来组成回收集(Collection Set,一般称 CSet)进行回收。衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多、回收收益最大。这就是 G1 收集器的 Mixed GC 模式。
当越来越多的对象晋升到老年代时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个新生代,还会回收一部分的老年代,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些 Old 区域进行收集,从而可以对垃圾回收的耗时时间进行控制。G1 没有 Full GC概念,需要 Full GC 时,调用 Serial Old GC 进行全堆扫描。
【3】G1 垃圾回收的过程
(1)初始标记(Initial Marking)
仅仅只是标记一下GC Roots能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这阶段需要停顿线程,但耗时很短。
TAMS:Top at Mark Start,Region 中的指针,用于并发标记时为对象分配内存空间。
(2)并发标记(Concurrent Marking)
从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象。此外,扫描完成后,还需要重新处理 STAB 记录下的在并发时有引用变动的对象。这阶段耗时较长,但可与用户程序并发执行。
STAB: Snapshot At The Begining,原始快照
如果发现有空的块(这里用红叉“X”标示的区域), 则会在 Remark 阶段立即移除。当然,”清单(accounting)”信息决定了活跃度的计算。
(3)最终标记(Final Marking)
是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中,这阶段需要停顿线程,但是可并行执行。
(4)筛选回收(Live Data Counting and Evacuation)
更新 Region 统计数据,对各个 Region 的回收价值和成本进行排序。
根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。
这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
【4】收集算法
(1)收集算法
整体:“标记整理算法”
局部:“标记复制算法”
G1 的这两种算法使其在运作期间不会产生内存空间碎片,垃圾收集完成后能提供规整的可用内存。而且这样有利于程序长时间运行(大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集)。
(2)年轻代GC过程
(3)老年代GC过程
(1)初始标记:stop-the-world,它伴随着一次普通的 Young GC 发生,然后对 Survivor 区(root region)进行标记,因为该区可能存在对老年代的引用。
(2)扫描根引用区:因为先进行了一次 YGC,所以当前年轻代只有 Survivor 区有存活对象,它被称为根引用区。扫描 Survivor 到老年代的引用,该阶段必须在下一次 Young GC 发生前结束。
(3)并发标记:寻找整个堆的存活对象,该阶段可以被 Young GC 中断。
(4)重新标记:stop-the-world,完成最后的存活对象标记。使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法。(Oracle 的资料显示,这个阶段会回收完全空闲的区块。)
(5)清理:清理阶段真正回收的内存很少。
到这里,G1 的一个并发周期就算结束了,其实就是主要完成了垃圾定位的工作,定位出了哪些分区是垃圾最多的。因为整堆一般比较大,所以这个周期应该会比较长,中间可能会被多次 stop-the-world 的 Young GC 打断。
(4)混合垃圾回收周期
并发周期结束后是混合垃圾回收周期,不仅进行年轻代垃圾收集,而且回收之前标记出来的老年代的垃圾最多的部分区块。
混合垃圾回收周期会持续进行,直到几乎所有的被标记出来的分区(垃圾占比大的分区)都得到回收,然后恢复到常规的年轻代垃圾收集,最终再次启动并发周期。
【5】总结
(1)特点总结
(1)G1垃圾回收器,将堆内存进行了更细致的划分和管理,分为一个个Region。每个Region大小是2的整数倍。可以理解,将堆内存划分成一个Region池子。这里的每个Region,可能属于新生代,也可能属于老年代使用。
(2)G1回收器对新生代和老年代都会进行工作,使用复制回收算法,可以高效,并且避免内存碎片的产生。
(3)G1还规划了一个大对象Region区域。这是G1特有的一个专门用来存放大对象的地方。默认超过1/2 Region大小的对象就是大对象。
(4)G1中,新生代和老年代的大小是不固定的,动态变化。
默认规定了新生代的大小初始值是占用5%Region,最大可以占用60%的Region。那么剩下的Region就是归老年代和大对象区域所有。具体占用多少也不固定,根据需要分配。
其次默认规定了老年代占用超过45%Region,就会触发MixedGC回收,可以理解为老年代回收。
(5)不管是YoungGC、还是MixedGC,都会触发大对象区域的回收。MixedGC还会触发Metaspace的回收。
G1的最大特点是,可以根据预设的期望停顿时间进行垃圾回收,对YoungGC、还是MixedGC都有效。而且发生垃圾回收的时机是不确定,完全由G1自己去判断,在恰当的时机,认为可以进行垃圾回收了,G1就会工作,但是每次回收会控制在默认期望停顿时间之内200ms。
(6)如果发生MixedGC过程中,如果发生了空间担保失败,或者因为又发生YoungGC,老年代没有足够空间存放新生代存活的对象,就会发生FullGC。这是停止一切工作线程的Serial垃圾回收的FullGC,这个是需要避免产生的。
(7)发生MixedGC的过程中,如果回收了Region。发生空闲的Region超过了-XX:G1HeapWastePercent=5%。就会停止本次的MixedGC,可能此次MixedGC耗时才100ms,也会停止。
(8)MixedGC过程中,也是分段执行的,可以通过-XX:G1MixedGCCountTarget=8进行控制,如果分为8次执行,那么每次STW的时间是25ms。
关于G1的调优(没有使用过G1回收器,只是根据CMS的一些想法,也参考RocketMQ broker启动JVM参数设置)
(2)一些提问问题
(1)并发标记如何实现的?如何保证收集线程与用户线程互不干扰地运行呢?
G1 收集器则是通过原始快照(STAB)算法实现的。此外,由于并发标记时用户线程仍在继续执行,肯定会持续创建新对象。
G1 为每个 Region 设计了两个名为 TAMS(Top at Mark Start)的指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配(默认都是存活的,不纳入回收范围)。
需要注意的是:如果内存回收速度赶不上内存分配的速度,G1 收集器也要被迫冻结用户线程执行,导致 Full GC 而产生长时间“Stop The World”。
(2)可靠的停顿模型
G1 收集器的停顿模型是以衰减均值(Decaying Average)为理论基础来实现的:垃圾收集过程中,G1 收集器会根据每个 Region 的回收耗时、记忆集中的脏卡数量等,分析得出平均值、标准偏差等。
“衰减平均值”比普通的平均值更能准确地代表“最近的”平均状态,通过这些信息预测现在开始回收的话,由哪些 Region 组成回收集才能在不超期望停顿时间的约束下获得最高收益。
【6】G1垃圾回收器的配置参数和测试案例
(1)参数规则
(1)可以使用默认的-XX:MaxGCPauseMills=200,期望STW时间,先不要去调整。
(2)调整MixedGC停止的条件-XX:G1HeapWastePercent=5。可以调整到10-15%吧。让一次MixedGC回收在停顿时间200ms以内,尽可能回收多一些Region。可以减少MixedGC的发生频率。
(3)如果堆内存比较大,例如16G或以上,可以设置单个Region的大小,例如16M。Region数量少一些,G1在回收的时候,或者跟踪每个Region是否满足回收条件,可能会更高效一点。
(4)InitiatingHeapOccupancyPercent=45 ,可以调整老年代占用超过45%Region触发MixedGC。这个值可以调整一下,例如30%,35%。更早的触发MixedGC回收过程。不让Region空闲比例很低,否则容易引起FullGC。
(5)G1ReservePercent=10 : G1为分配担保预留的空间比例,默认10%,这个可以适当调节20%。预留更多的空间,进行YoungGC的空间担保,防止Serial的FullGC产生。
(6)其他参数尽量不要调整,可以根据线上压测情况,和实际生产情况。
主要是跟踪停顿时间指标,这个200ms设置,YoungGC的发生频率如何,MixedGC发生频率如何,可以通过一些GC分析工具,例如在线的https://gceasy.io/。看一下吞吐量占比,如果99%以上,平均耗时等指标,如果正常其实没必要进行过多的调优。
(2)参数设置模板
#堆内存最大最小值为4g,新生代内存2g
-Xms4g -Xmx4g -Xmn2g
#元空间128m,最大320m
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=320m
#开启远程debug
-Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n
#使用G1垃圾收集器,在低延迟和高吞吐间寻找平衡,可以调整最大停止时间,设置新生代大小来提高吞吐量,让出cpu资源
-XX:+UseG1GC
#设置最大暂停时间,默认200ms
-XX:MaxGCPauseMillis=200
#指定Region大小,必须是2次幂
-XX:G1HeapRegionSize=2m
#反复执行混合回收8次,每次回收受MaxGCPauseMillis的影响可能一次性回收不了所有垃圾,增加次数才能回收的更彻底
-XX:G1MixedGCCountTarget=8
# 混合回收整理出来的空闲空间占heap的10时,结果老年代的回收,默认5
-XX:G1HeapWastePercent=10
#设置新生代大小,最大60%,默认5%
-XX:G1NewSizePercent=10 -XX:G1MaxNewSizePercent=50
-XX:SurvivorRatio=8
#在控制台输出GC情况
-verbose:gc
#gc日志打印到执行日志文件
-Xloggc:./logs/job_execute_gc.log
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime
#可以生成更详细的Survivor空间占用日志
-XX:+PrintAdaptiveSizePolicy
#jdk 1.6开始,默认server模式下开启了这个参数,意为当jvm检测到程序在重复抛一个异常,在执行若干次后会将异常吞掉
-XX:-OmitStackTraceInFastThrow
-XX:-UseLargePages
#指定加载配置文件
--spring.config.location=classpath:/,classpath:/config/,file:./,file:./config/,file:/home/mall-job/conf/
#---当前分布式任务调度采用jvm参数,-Xmn2g,-XX:MaxGCPauseMillis=400调整新生代内存大小,增大暂停时间提高吞吐量---------------------------
-Xms4g
-Xmx4g
-Xmn2g
-Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n
-XX:+UseG1GC
-XX:MaxGCPauseMillis=400
-XX:G1HeapRegionSize=2m
-XX:G1MixedGCCountTarget=8
-XX:G1MixedGCCountTarget=8
-verbose:gc
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCApplicationStoppedTime
-Xloggc:/logs/execute/mall-job-execute-gc.log
【7】G1和CMS相比
(1)G1的优点
(1)停顿时间可控
G1可预测停顿时间,用户可以指定垃圾回收时间的限时,兼顾高吞吐量。G1可以根据设定的预期stop the world时间,来选择最少回收时间和最多回收对象的Region进行垃圾回收,在保证GC对系统停顿的可控时间内,同时还会尽可能的回收最多的对象。
(2)算法没有空间碎片
G1整体是标记整理算法,Region之间是复制算法,收集完垃圾后得到的空间保证是连续的,不会产生空间碎片,分配大对象时不会无法得到连续的空间而提前触发一次FULL GC。
(3)并行与并发
G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
(2)CMS和G1的区别
(1)CMS中,堆被分为PermGen,YoungGen,OldGen;而YoungGen又分了两个survivo区域。在G1中,堆被平均分成几个区域(region),在每个区域中,虽然也保留了新老代的概念,但是收集器是以整个区域为单位收集的。
(2)G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做。
(3)G1会在Young GC中使用、而CMS只能在O区使用。
(二)CMS垃圾回收器
【1】CMS的内存模型
【2】CMS垃圾回收的过程
CMS可以简单分为5个步骤:初始标记、并发标记、(并发预清理)、重新标记以及并发清除、并发重置
(1)初始标记
初始标记其实就是对被我们GC ROOT直接引用的对象做一个标记,而在这个过程中将会触发一次STW机制。在这个过程中,虽然会触发STW机制,但是时间会非常的短。
(2)并发标记
在进行并发标记的过程中,我们的用户线程和CMS线程会一起执行。CMS所做的一件事情就是把堆里的所有引用对象全部找到并做标记。
但是在这个过程中可能会发生对象状态被改变的问题。
(1)多标问题
比如我的一个对象的引用链已经断开,变成了垃圾对象,但是CMS已经对他做过标记判断为非垃圾对象了怎么办?
(2)漏标问题
又比如本来一个对象在CMS标记的过程中把他标记成了垃圾对象但是后来我们有引用了,结果在我们用的时候垃圾对象已经被干掉了,那我们是不是在引用这个对象的时候就会找不到这个垃圾对象。这时候我们的第三步就产生了。
(3)重新标记
在这一步,CMS会触发STW机制,并修复并发标记状态已经改变的对象,但是这个过程会比较漫长。他利用三色标记和增量更新来解决我们的漏标问题。
(4)并发清除
那这一步就很好理解了,所谓的并发清理其实就是对没有被做标记的对象进行一个清理回收,在这个过程中同样不会产生STW。
(5)并发重置
重置本次GC过程中的标记数据
【3】收集算法
分代收集算法
(1)年轻代的回收算法 (回收主要以”复制算法“为主)
(1)所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
(2)新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当创建对象,向Eden区申请内存时,如果Eden区满了,就进行minor GC。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时 survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
(3)当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC(Major GC),也就是新生代、老年代都进行回收。
(4)新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
(2)年老代的回收算法(回收主要以”标记-整理算法“为主)
(1)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
(2)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
(3)minor gc和full gc发生的时机
(1)Minor GC触发条件:当Eden区满时,触发Minor GC。
(2)Full GC触发条件:
1、调用System.gc时,系统建议执行Full GC,但是不必然执行
2、老年代空间不足
3、方法区(1.8之后改为元空间)空间不足
4、创建大对象,比如数组,通过Minor GC后,进入老年代的平均大小大于老年代的可用内存
5、由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
【4】总结
(1)CMS的缺点
(1)空间需要预留
CMS垃圾收集器可以一边回收垃圾,一边处理用户线程,那需要在这个过程中保证有充足的内存空间供用户使用。如果CMS运行过程中预留的空间不够用了,会报错(Concurrent Mode Failure),这时会启动 Serial Old垃圾收集器进行老年代的垃圾回收,会导致停顿的时间很长。显然啦,空间预留多少,肯定是有参数配置的。
(2)无法处理浮动垃圾
由于垃圾回收和用户线程是同时进行的,在进行标记或者清除的同时,用户的线程还会去改变对象的引用,使得原来某些对象不是垃圾,但是当 CMS 进行清理的时候变成了垃圾,CMS 收集器无法收集,只能等到下一次 GC。CMS 收集器无法处理浮动垃圾(Floating Garbage),可能出现 “Concurrent Mode Failure” 失败而导致另一次 Full GC 的产生。如果在应用中老年代增长不是太快,可以适当调高参数 - XX:CMSInitiatingOccupancyFraction 的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能。
(3)会产生大量的空间碎片
CMS本质上是实现了标记清除算法的收集器(从过程就可以看得出),这会意味着会产生内存碎片。由于碎片太多,又可能会导致内存空间不足所触发full GC,CMS一般会在触发full GC这个过程对碎片进行整理。整理涉及到移动/标记,那这个过程肯定会Stop The World的,如果内存足够大(意味着可能装载的对象足够多),那这个过程卡顿也是需要一定的时间的。
(4)回收时间长,吞吐量不如Parallel
由于我们在执行CMS垃圾回收器的过程中有一部分资源让给了用户线程,那就会导致回收时间长,内存空间没有被及时释放掉也就会导致吞吐量不如Paralle
(5)并发模式失败:Concurrent model failure
当我们的老年代内存空间满了之后,虽然这时候在做FULL GC,但是在FULL GC的过程中有一个新的对象进来了怎么办?
此时会进入STW状态,并且CMS会自动切换到用Serial old垃圾收集器来回收。Serial我们都知道,它是一个单线程的垃圾回收器。那这种情况出现是不是会严重降低我们的执行效率?
那么我们为了解决这个问题,可以通过调整老年代空间被占满了多少之后触发FULL GC
# 通过这个参数可以调整触发full gc的百分比,默认是92%
-XX:CMSInitiatingOccupancyFraction