JVM进阶调优系列(5)CMS回收器通俗演义一文讲透FullGC

本文涉及的产品
云原生网关 MSE Higress,422元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 本文介绍了JVM中CMS垃圾回收器对Full GC的优化,包括Stop the world的影响、Full GC触发条件、GC过程的四个阶段(初始标记、并发标记、重新标记、并发清理)及并发清理期间的Concurrent mode failure处理,并简述了GC roots的概念及其在GC中的作用。

最近读书学习心理健康相关知识,学到了三个当下打工人常见状态概念:

1、悲伤、抑郁、焦虑,分别制造了过去之熵,现在之熵、未来之熵。心理情绪熵减调理,需要解决掉这些负熵。

     JVM的调优,重中之中就是FullGC的优化。FullGC由于Stop the world耗时大,快的的几秒,慢的几十秒,对业务的正常运行造成了负面影响。本文主角CMS垃圾回收器在对FullGC做了充足的优化,值得深入探讨学习。

一、让人头疼的Stop the world

    Stop the world发生后,jvm程序的全部线程暂停运行,不能创建对象,让垃圾回收器专注清理垃圾对象。此时我们的接口响应被迫延迟,最终传导影响业务客户系统丝滑体验。在G1回收器出现之前,这个stop the world一直是JVM的最大痛点。

二、什么时候会触发FullGC?

   YGC也会stop the world,只是YGC耗时短,影响不明显。这里不赘述YGC的时机,之前文章已分享过,这里详细分析FullGC触发时机。

2.1 空间担保机制

   JDK8默认是开启。具体就是如果当前老年代可用空间小于年轻代全部对象的大小,由于有空间担保机制,不会马上发生Full GC,而会再看看其他条件。如果没有空间担保机制,就会马上发生Full GC。

2.2 比较历次YGC存活进入老年代的平均对象大小

    当前老年代可用空间小于历次YGC后进入老年代的平均对象大小,会发生Full GC。

2.3 比较老年代可用空间

   YGC存活的对象过大,S区存放不小,而且老年代可用空闲空间也不放心,就发生Full GC。

2.4 检查-XX:CMSInitiatingOccupancyFaction参数

   这个值默认是92%。如果配置该参数即使老年代可用空间大于历次YGC后进入老年代的平均对象大小,但是当前老年代占用大于92%空间,CMS垃圾回收器也会进行FullGC。配这个参数的好处在于,老年代有预留的8%空间,让JVM在CMS并发回收期间,系统还可以继续把一些新对象存入老年代。

三、为什么Full GC慢?

      这个之前有分享过,就是并发整理过程比较慢,涉及GC roots追踪对象,以及内存碎片整理。这两个过程很复杂,然后关于内存碎片整理,有2个参数有必要说一下。

3.1 -XX:+UseCMSCompactAtFullCollection

   默认是打开。顾名思义这个参数的作用就是用来做内存碎片整理。具体就是CMS 完成Full GC后,再次进行stop the world,然后将存活对象挪到一起,空出来一片连续内存,避免内存碎片。

3.2 -XX:+CMSFullGCCsBeforeCompaction

   CMS垃圾回收器Full GC多少次后才开始做内存碎片整理,默认就是0,意味着每次FGC后都整理碎片。这个是官方推荐,也是比较合适的。如果设置为5,就是5次Full GC后才进行内存碎片整理,会导致老年代在前5次Full GC后有比较多的内存碎片,这个参数需要慎重考虑优化配置。

   那导致Full GC慢的另一个原因:GC roots追踪,又是什么呢?

四、GC roots是什么?

   GC Roots是垃圾收集器可以访问的引用对象,通过这些对象可以找到其他所有可达的对象。比如:虚拟机栈中的对象引用(典型的局部变量、方法里new实例对象名地址):之前文章说过每个线程都有一个私有的Java虚拟机栈,该线程执行每个方法的调用执行,都会将一个栈帧在线程的虚拟机栈中入栈出栈。如果一个变量、或实例对象在方法执行过程中被引用,则该对象可以作为GC Roots,以及static修饰的类静态变量都是GC Roots。具体还有很多这里不赘述。

五、对对象进行GC Roots追踪和GC roots标记有啥区别?

    GC roots标记,就是遍历GC roots,看看他们引用的对象是哪些。也就是通过GC roots根对象引用,去找具体对象。而【对象进行GC Roots追踪】,刚好相反。这是通过遍历全部对象,然后看看每个对象是否有对应GC roots引用。

    理解了这两点,一会看CMS的四阶段GC处理就很方便。

六、CMS垃圾回收器处理FGC的四个阶段

6.1 初始标记

   首先stop the world,系统的工作线程全部暂停。CMS开始初始标记,这个阶段就是标记出所有GC roots引用的对象。虽然导致代码程序暂停,但是CMS的垃圾回收是多个回收线程并发执行,默认回收线程数量=(系统cpu核数+3)/4,标记效率很高,这个阶段对系统几乎无感影响。

   比如以下代码,执行到方法gcRootsHere()的System.out.println发生了FGC,那初始阶段就是回收线程,在老年代区域根据类静态变量name,userA,还有和法局部变量user的地址引用,去把这些GC roots引用的对象标记出来。


public class Demo002JvmShow {
    public static final String name = "我是类静态变量";
    public static User userA = new User("A");
    private Object object = new Object();
    private boolean isOk = 10 / 2 == 4 ? true : false;
    public void gcRootsHere(){
        int a = 5;
        User user = new User("我是实例对象,我有GC roots引用我,不许回收我");
        System.out.println("线程执行到这里,发送了FullGC .....");
    }

6.2 并发标记

   这个阶段,运行程序从暂停状态stop the world 变成继续运行,以及继续做标记。这个阶段些微复杂一些,因为这时候系统继续运行,肯定就会产生新的对象,也有的对象变成了垃圾对象。回收线程,这时候的标记和阶段1【初始标记】有所不同。这时候是对老年代所有对象进行GC roots追踪。什么意思呢?

    就是遍历老年代的对象,看看这个对象被谁引用,一级级往上找,看最终是否有被GC roots引用。这个是不是有点意思,和初始标记反着来?一个是从上往下,一个是从下往上。

    这个阶段非常耗时,一个是因为老年代发生FGC说明老年代对象很多,另外一个就是追踪链路很长,一层层去追踪,向上找是否有被GC roots引用。如果没有被引用就是垃圾对象,一会可以回收。

   虽然很耗时,但是好在是程序恢复了继续运行,也就影响不大。

6.3 重新标记

   并发标记结束后,由于程序恢复运行,产生了很多新对象,最重要的是有很多对象在这期间变成了垃圾对象,这时候就需要重新标记把他们标出来,也一并回收。

   重新标记阶段,采用的是stop the world,这样就可以避免有新垃圾产生,彻底标记垃圾对象。

   这个阶段,运行也是非常快,原因是只对阶段二程序继续运行影响到的对象进行标记,这个数量肯定是比较少。所以这次stop the world也影响不大。看完这里,不得不佩服CMS的优秀设计,对FGC的优化真是做到极致。

6.4 并发清理

   经历了【初始标记】【并发标记】【重新标记】三个阶段标记,所有垃圾对象都标记出来,那开始做清理了。

   这个阶段,竟然是并发,就是允许程序工作线程恢复运行。系统一边运行,CMS垃圾回收器对垃圾对象进行回收整理。这个阶段实际是非常耗时,比如整理碎片,整理存活对象。但是因为程序可以继续运行,那影响也很小。

七、正在进行FullGC,老年代放不下程序产生的新对象就会直接OOM了吗?

   我们知道在【并发清理】这个期间,程序是可以正常运行的。原因是老年代在回收碎片,程序依然可以继续往里面放新对象。此时如果系统要往老年代放新对象,放不下(为啥会往老年代放新对象?这个看过前几篇的文章同学就知道,有些对象是可以直接进老年代的),这时候会发生Concurrent mode failure,而不是OOM。这时候JVM自动使用Serial Old垃圾回收器替换CMS回收器,强行让程序stop the word,程序暂停运行,不允许新对象生成。直到GC完成后再恢复。

八、看看这份JVM调优入参,分别代表什么?

-Xmx4096, -Xms2048,  -Xmn2048 -Xss2m -XX:SurvivorRatio=8 
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m
 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC 
 XX:MaxTenuringThreshold=10  -XX:CMSInitiatingOccupancyFaction 
 -XX:+PrintGC  -XX:+HeapDumpOnOutOfMemoryError 
 -XX:HeapDumpPath=${LOGDIR}/-

这个问题我们下一篇给探讨分析。

推荐阅读:

1、JVM进阶调优系列(3)堆内存的对象什么时候被回收?

2、JVM进阶调优系列(2)字节面试:JVM内存区域怎么划分,分别有什么用?

3、JVM进阶调优系列(1)类加载器原理一文讲透

4、JAVA并发编程系列(13)Future、FutureTask异步小王子

相关文章
|
16天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
23天前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
27 1
|
26天前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
27天前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
48 1
|
1月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
1月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
1月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
27天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
211 1
|
2月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
40 4
|
17天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。