[转发]Full GC次数过多问题的排查思路

本文介绍了线上可能出现的五种导致系统缓慢的情况,包括Full GC次数过多、CPU过高、不定期接口耗时、线程进入WAITING状态和死锁。详细分析了每种情况的现象,并给出了相应的排查定位方法,为解决线上系统缓慢问题提供了思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

 

1. Full GC次数过多

     相对来说,这种情况是最容易出现的,尤其是新功能上线时。对于Full GC较多的情况,其主要有如下两个特征:

  • 线上多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程

  • 通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加。

   首先我们可以使用top命令查看系统CPU的占用情况,如下是系统CPU较高的一个示例:

可以看到,有一个Java程序此时CPU占用量达到了98.8%,此时我们可以复制该进程id9,并且使用如下命令查看呢该进程的各个线程运行情况:

可以看到,在进程为9的Java程序中各个线程的CPU占用情况,接下来我们可以通过jstack命令查看线程id为10的线程为什么耗费CPU最高。需要注意的是,在jsatck命令展示的结果中,线程id都转换成了十六进制形式。可以用如下命令查看转换结果,也可以找一个科学计算器进行转换:

可以看到,这里FGC指的是Full GC数量,这里高达6793,而且还在不断增长。从而进一步证实了是由于内存溢出导致的系统缓慢。那么这里确认了内存溢出,但是如何查看你是哪些对象导致的内存溢出呢,这个可以dump出内存日志,然后通过eclipse的mat工具进行查看,如下是其展示的一个对象树结构

经过mat工具分析之后,我们基本上就能确定内存中主要是哪个对象比较消耗内存,然后找到该对象的创建位置,进行处理即可。这里的主要是PrintStream最多,但是我们也可以看到,其内存消耗量只有12.2%。也就是说,其还不足以导致大量的Full GC,此时我们需要考虑另外一种情况,就是代码或者第三方依赖的包中有显示的System.gc()调用。这种情况我们查看dump内存得到的文件即可判断,因为其会打印GC原因:

比如这里第一次GC是由于System.gc()的显示调用导致的,而第二次GC则是JVM主动发起的。总结来说,对于Full GC次数过多,主要有以下两种原因:

  • 代码中一次获取了大量的对象,导致内存溢出,此时可以通过eclipse的mat工具查看内存中有哪些对象比较多;

  • 内存占用不高,但是Full GC次数还是比较多,此时可能是显示的System.gc()调用导致GC次数过多,这可以通过添加-XX:+DisableExplicitGC来禁用JVM对显示GC的响应。

2. CPU过高

在前面第一点中,我们讲到,CPU过高可能是系统频繁的进行Full GC,导致系统缓慢。而我们平常也肯能遇到比较耗时的计算,导致CPU过高的情况,此时查看方式其实与上面的非常类似。首先我们通过top命令查看当前CPU消耗过高的进程是哪个,从而得到进程id;然后通过top -Hp <pid>来查看该进程中有哪些线程CPU过高,一般超过80%就是比较高的,80%左右是合理情况。这样我们就能得到CPU消耗比较高的线程id。接着通过该线程id的十六进制表示jstack日志中查看当前线程具体的堆栈信息。

在这里我们就可以区分导致CPU过高的原因具体是Full GC次数过多还是代码中有比较耗时的计算了。如果是Full GC次数过多,那么通过jstack得到的线程信息会是类似于VM Thread之类的线程,而如果是代码中有比较耗时的计算,那么我们得到的就是一个线程的具体堆栈信息。如下是一个代码中有比较耗时的计算,导致CPU过高的线程信息:

 

这里可以看到,在请求UserController的时候,由于该Controller进行了一个比较耗时的调用,导致该线程的CPU一直处于100%。我们可以根据堆栈信息,直接定位到UserController的34行,查看代码中具体是什么原因导致计算量如此之高。

3. 不定期出现的接口耗时现象

对于这种情况,比较典型的例子就是,我们某个接口访问经常需要2~3s才能返回。这是比较麻烦的一种情况,因为一般来说,其消耗的CPU不多,而且占用的内存也不高,也就是说,我们通过上述两种方式进行排查是无法解决这种问题的。而且由于这样的接口耗时比较大的问题是不定时出现的,这就导致了我们在通过jstack命令即使得到了线程访问的堆栈信息,我们也没法判断具体哪个线程是正在执行比较耗时操作的线程。

对于不定时出现的接口耗时比较严重的问题,我们的定位思路基本如下:首先找到该接口,通过压测工具不断加大访问力度,如果说该接口中有某个位置是比较耗时的,由于我们的访问的频率非常高,那么大多数的线程最终都将阻塞于该阻塞点,这样通过多个线程具有相同的堆栈日志,我们基本上就可以定位到该接口中比较耗时的代码的位置。如下是一个代码中有比较耗时的阻塞操作通过压测工具得到的线程堆栈日志:

 

从上面的日志可以看你出,这里有多个线程都阻塞在了UserController的第18行,说明这是一个阻塞点,也就是导致该接口比较缓慢的原因。

 

4. 某个线程进入WAITING状态

对于这种情况,这是比较罕见的一种情况,但是也是有可能出现的,而且由于其具有一定的“不可复现性”,因而我们在排查的时候是非常难以发现的。笔者曾经就遇到过类似的这种情况,具体的场景是,在使用CountDownLatch时,由于需要每一个并行的任务都执行完成之后才会唤醒主线程往下执行。而当时我们是通过CountDownLatch控制多个线程连接并导出用户的gmail邮箱数据,这其中有一个线程连接上了用户邮箱,但是连接被服务器挂起了,导致该线程一直在等待服务器的响应。最终导致我们的主线程和其余几个线程都处于WAITING状态。

对于这样的问题,查看过jstack日志的读者应该都知道,正常情况下,线上大多数线程都是处于TIMED_WAITING状态,而我们这里出问题的线程所处的状态与其是一模一样的,这就非常容易混淆我们的判断。解决这个问题的思路主要如下:

  • 通过grep在jstack日志中找出所有的处于TIMED_WAITING状态的线程,将其导出到某个文件中,如a1.log,如下是一个导出的日志文件示例:

"Attach Listener" #13 daemon prio=9 os_prio=31 tid=0x00007fe690064000 nid=0xd07 waiting on condition [0x0000000000000000]
"DestroyJavaVM" #12 prio=5 os_prio=31 tid=0x00007fe690066000 nid=0x2603 waiting on condition [0x0000000000000000]
"Thread-0" #11 prio=5 os_prio=31 tid=0x00007fe690065000 nid=0x5a03 waiting on condition [0x0000700003ad4000]
"C1 CompilerThread3" #9 daemon prio=9 os_prio=31 tid=0x00007fe68c00a000 nid=0xa903 waiting on condition [0x0000000000000000]
  • 等待一段时间之后,比如10s,再次对jstack日志进行grep,将其导出到另一个文件,如a2.log,结果如下所示:

"DestroyJavaVM" #12 prio=5 os_prio=31 tid=0x00007fe690066000 nid=0x2603 waiting on condition [0x0000000000000000]
"Thread-0" #11 prio=5 os_prio=31 tid=0x00007fe690065000 nid=0x5a03 waiting on condition [0x0000700003ad4000]
"VM Periodic Task Thread" os_prio=31 tid=0x00007fe68d114000 nid=0xa803 waiting on condition
  • 重复步骤2,待导出3~4个文件之后,我们对导出的文件进行对比,找出其中在这几个文件中一直都存在的用户线程,这个线程基本上就可以确认是包含了处于等待状态有问题的线程。因为正常的请求线程是不会在20~30s之后还是处于等待状态的。

  • 经过排查得到这些线程之后,我们可以继续对其堆栈信息进行排查,如果该线程本身就应该处于等待状态,比如用户创建的线程池中处于空闲状态的线程,那么这种线程的堆栈信息中是不会包含用户自定义的类的。这些都可以排除掉,而剩下的线程基本上就可以确认是我们要找的有问题的线程。通过其堆栈信息,我们就可以得出具体是在哪个位置的代码导致该线程处于等待状态了。

这里需要说明的是,我们在判断是否为用户线程时,可以通过线程最前面的线程名来判断,因为一般的框架的线程命名都是非常规范的,我们通过线程名就可以直接判断得出该线程是某些框架中的线程,这种线程基本上可以排除掉。而剩余的,比如上面的Thread-0,以及我们可以辨别的自定义线程名,这些都是我们需要排查的对象。

经过上面的方式进行排查之后,我们基本上就可以得出这里的Thread-0就是我们要找的线程,通过查看其堆栈信息,我们就可以得到具体是在哪个位置导致其处于等待状态了。如下示例中则是在SyncTask的第8行导致该线程进入等待了。

"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9de08c7000 nid=0x5603 waiting on condition [0x0000700001f89000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
    at com.aibaobei.chapter2.eg4.SyncTask.lambda$main$0(SyncTask.java:8)
    at com.aibaobei.chapter2.eg4.SyncTask$$Lambda$1/1791741888.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)

5. 死锁

对于死锁,这种情况基本上很容易发现,因为jstack可以帮助我们检查死锁,并且在日志中打印具体的死锁线程信息。如下是一个产生死锁的一个jstack日志示例:

6. 小结

本文主要讲解了线上可能出现的五种导致系统缓慢的情况,详细分析了每种情况产生时的现象,已经根据现象我们可以通过哪些方式定位得到是这种原因导致的系统缓慢。简要的说,我们进行线上日志分析时,主要可以分为如下步骤:

  • 通过 top命令查看CPU情况,如果CPU比较高,则通过top -Hp <pid>命令查看当前进程的各个线程运行情况,找出CPU过高的线程之后,将其线程id转换为十六进制的表现形式,然后在jstack日志中查看该线程主要在进行的工作。这里又分为两种情况

  • 如果是正常的用户线程,则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU;

  • 如果该线程是VM Thread,则通过jstat -gcutil <pid> <period> <times>命令监控当前系统的GC状况,然后通过jmap dump:format=b,file=<filepath> <pid>导出系统当前的内存数据。导出之后将内存情况放到eclipse的mat工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码;

  • 如果通过 top 命令看到CPU并不高,并且系统内存占用率也比较低。此时就可以考虑是否是由于另外三种情况导致的问题。具体的可以根据具体情况分析:

  • 如果是接口调用比较耗时,并且是不定时出现,则可以通过压测的方式加大阻塞点出现的频率,从而通过jstack查看堆栈信息,找到阻塞点;

  • 如果是某个功能突然出现停滞的状况,这种情况也无法复现,此时可以通过多次导出jstack日志的方式对比哪些用户线程是一直都处于等待状态,这些线程就是可能存在问题的线程;

  • 如果通过jstack可以查看到死锁状态,则可以检查产生死锁的两个线程的具体阻塞点,从而处理相应的问题。

本文主要是提出了五种常见的导致线上功能缓慢的问题,以及排查思路。当然,线上的问题出现的形式是多种多样的,也不一定局限于这几种情况,如果我们能够仔细分析这些问题出现的场景,就可以根据具体情况具体分析,从而解决相应的问题

<think>好的,我现在需要回答用户的问题Full GC次数过多是否会移除我的Executor,然后YARN重新分配一个Executor。首先,我需要理解用户的问题背景。用户可能在使用Apache Spark或类似的分布式计算框架,运行在YARN资源管理器上。问题涉及到JVM的垃圾回收机制(Full GC)与资源管理(YARN)之间的交互。 首先,我应该回顾一下Full GC对应用程序的影响。Full GC会暂停应用程序线程(STW),导致延迟。如果Full GC频繁发生,说明内存回收存在问题,可能因为内存不足或对象生命周期管理不当。频繁Full GC会导致应用程序响应变慢,甚至长时间停顿,这可能被集群管理器误认为节点无响应。 接下来,需要了解YARN如何管理Executor的生命周期。YARN作为资源管理器,负责启动和监控容器(包括Executor)。如果Executor进程异常退出或无法响应心跳,YARN会将其标记为失败并尝试重新分配。但Full GC本身是否会导致YARN主动移除Executor呢? 可能的机制是:频繁Full GC导致Executor进程的停顿时间过长,无法及时向YARN发送心跳信号。YARN的心跳机制有超时设置,如果超过一定时间未收到心跳,YARN会认为该Executor失效,从而终止容器并重新分配。此外,如果Full GC导致Executor进程内存溢出(OOM),进程崩溃,YARN自然也会重新启动。 需要注意的关键点包括: 1. YARN的心跳超时时间配置,如yarn.nm.liveness-monitor.expiry-interval-ms,默认是10分钟。如果Full GC导致Executor在这段时间内无法发送心跳,YARN会将其标记为死亡。 2. JVM的OOM错误,如果Full GC后仍然无法释放足够内存,会抛出OOM,导致Executor进程退出,触发YARN重新分配。 3. Spark自身的配置,如spark.executor.extraJavaOptions中的GC相关参数,可能影响Full GC的频率和持续时间。 需要验证这些机制是否正确,例如查看YARN和Spark的官方文档,确认心跳机制和容器失效的条件。此外,用户可能需要检查日志,确认Executor退出的原因是否是OOM或心跳超时。 总结回答结构:先说明Full GC次数过多的影响,解释YARN如何监控Executor,分析Full GC导致移除的可能原因(心跳超时或OOM),最后给出调优建议。</think>### 🚨 **核心结论** **会!频繁Full GC可能导致YARN主动移除Executor并重新分配**,主要触发机制是**心跳超时检测**或**OOM崩溃**。 --- ### 📉 **根本原因分析** #### **场景1:心跳超时(更常见)** - **YARN监控机制**: - NodeManager周期性(默认1秒)检测Executor进程心跳 - 连续丢失心跳超过 `yarn.nm.liveness-monitor.expiry-interval-ms`(默认600,000ms=10分钟)→ **标记为死亡** - **Full GC影响**: - 单次Full GC可能导致**分钟级STW停顿**(如CMS/G1混合GC失败时) - 若10分钟内发生多次Full GC → **累计心跳超时** → YARN强制kill容器 #### **场景2:OOM崩溃(直接致命)** - Full GC后仍无法释放足够内存 → 抛出 `java.lang.OutOfMemoryError: Java heap space` - Executor进程异常退出 → YARN立即重新分配新容器 --- ### 🔍 **典型日志证据** ```log // 心跳超时引发的移除 WARN org.apache.hadoop.yarn.server.nodemanager.containermanager.ContainerManagerImpl: Container container_xxxx exited with status 143 (被主动终止) // OOM引发的崩溃 ERROR org.apache.spark.executor.Executor: Exception in task java.lang.OutOfMemoryError: Java heap space ``` --- ### ⚙️ **调优关键参数** #### **防御Full GC** | 参数 | 作用 | 推荐值示例 | |----------------------------------------|-----------------------------|-------------------| | `-XX:+UseG1GC` | 启用低停顿G1回收器 | 必选 | | `-XX:InitiatingHeapOccupancyPercent=35`| 提前触发GC避免堆占满 | 35~45(根据负载调整) | | `-XX:G1ReservePercent=15` | 预留内存防晋升失败 | 10~20 | #### **缓解YARN误杀** | 参数 | 作用 | 推荐值 | |-------------------------------------------|-----------------------------|------------| | `yarn.nm.liveness-monitor.expiry-interval-ms` | 心跳超时阈值 | 1200000(20分钟) | | `spark.executor.heartbeatInterval` | Executor心跳间隔 | 10s → 5s | --- ### 🌰 **真实案例** 某电商实时推荐系统出现Executor频繁被YARN重启: - **现象**:每小时发生3~4次Executor重新分配 - **排查**: 1. GC日志显示老年代每小时Full GC 15次 2. YARN日志显示 `CONTAINER_KILLED_ON_EXCESSIVE_GC` - **解决方案**: ```bash # 调整G1参数减少Full GC --conf spark.executor.extraJavaOptions="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1ReservePercent=20" # 放宽YARN GC超时判定 <property> <name>yarn.nm.container-monitor.resource-calculator.class</name> <value>org.apache.hadoop.yarn.util.LinuxResourceCalculator</value> </property> <property> <name>yarn.nm.container-monitor.procfs-tree.smaps-based-rss.enabled</name> <value>false</value> </property> ``` --- ### 📌 **行动清单** 1. **优先减少Full GC**: - 分析堆转储(`jmap -histo`)查找内存泄漏 - 升级G1/CMS以外的低延迟GC(如ZGC/Shenandoah) 2. **次优调整超时阈值**: ```xml <!-- yarn-site.xml --> <property> <name>yarn.nm.liveness-monitor.expiry-interval-ms</name> <value>1200000</value> </property> ``` 3. **终极方案**: - 若业务允许,直接增加Executor内存(`spark.executor.memory=8g → 12g`) - 拆分大任务避免单个Executor负载过高 --- **最终结论**:Full GC是Executor被YARN杀死的**间接推手**,本质上需要优化JVM GC或调整YARN/Spark的敏感性参数! 🔧
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值