Full GC 频繁怎么排查

Full GC 频繁通常说明老年代或者元空间压力比较大,也可能是代码或参数配置导致 GC 退化。

排查时先看三个东西:

1
2
3
Full GC 发生频率
单次停顿时间
Full GC 之后内存是否明显下降

如果 Full GC 之后老年代占用明显下降,比如:

1
2
GC 前:90%
GC 后:30%

说明对象大部分其实是可以回收的,更像是对象创建太快、堆太小,或者大量对象过早晋升到了老年代。

这时可以继续看:

1
2
3
4
Young GC 是否过于频繁
对象晋升速度是否过快
新生代是否太小
是否存在大量大对象

如果 Full GC 之后老年代几乎不下降:

1
2
GC 前:90%
GC 后:85%

而且后面还在持续上涨,就要重点怀疑:

大量对象长期存活,甚至存在内存泄漏。

这时候通常需要导出 Heap Dump,分析:

1
2
3
哪些对象占用内存最多
哪些对象数量异常
这些对象为什么一直被 GC Roots 引用

另外还要关注 Full GC 的触发原因,比如:

1
2
3
4
5
老年代空间不足
元空间不足
System.gc()
CMS Concurrent Mode Failure
G1 回收失败或者发生退化

所以 Full GC 频繁的整体排查思路可以概括成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
先确认 Full GC 是否真的频繁
→ 看 GC 后内存能不能明显下降

能下降
→ 更偏向容量、对象分配速度、晋升问题

降不下来
→ 更偏向长期存活对象或内存泄漏

最后结合 GC 日志和 Heap Dump 定位

OOM 怎么排查

OutOfMemoryError 不只代表 Java 堆满了。

第一步应该先看 OOM 的具体类型。

常见的情况包括:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Java heap space
→ Java 堆内存不足

Metaspace
→ 元空间不足

Direct buffer memory
→ 直接内存不足

unable to create native thread
→ 创建线程失败,本地内存或系统线程资源不足

如果是:

1
OutOfMemoryError: Java heap space

重点看堆里的对象。

先判断是内存容量不够还是内存泄漏。

如果程序确实需要长期保存大量数据,GC 后内存可以正常下降,只是堆容量确实太小,那更像是容量问题。

如果 GC 后仍然有大量本应消失的对象存活,就要怀疑内存泄漏。

通常会在 OOM 时自动生成 Heap Dump:

1
-XX:+HeapDumpOnOutOfMemoryError

然后通过 MAT 等工具分析。

主要关注:

1
2
3
4
5
哪些对象占用内存最大
哪些类的实例数量异常
Dominator Tree
Retained Size
GC Roots 引用链

其中最关键的是找:

为什么这个对象明明不用了,却还被某条引用链一直引用着。

例如常见的泄漏来源:

1
2
3
4
5
静态集合不断增长
缓存没有淘汰
ThreadLocal 忘记 remove
监听器注册后没有注销
连接、会话、任务对象长期保存

如果是 Metaspace OOM,则重点排查:

1
2
3
4
是否加载了大量 Class
是否频繁创建新的 ClassLoader
是否存在 ClassLoader 泄漏
动态生成类是否过多

如果是直接内存 OOM,则重点看:

1
2
3
DirectByteBuffer
Netty 等框架的直接内存使用
直接内存是否没有及时释放

所以 OOM 排查的核心不是一上来就加大 -Xmx,而是:

先确定是哪块内存 OOM,再判断到底是容量不够,还是对象/资源本该释放却没有释放。


CPU 飙高怎么定位到具体线程

CPU 飙高时,首先要确定到底是不是 Java 进程占用了大量 CPU。

可以先通过系统命令找到 Java 进程:

1
top

找到对应 PID 以后,再查看这个进程内部哪些线程 CPU 占用最高:

1
top -Hp <pid>

这时可以得到一个线程 ID,例如:

1
TID = 12345

接下来需要把这个线程 ID 转成十六进制,因为 jstack 里的线程通常使用十六进制 nid:

1
2
12345
→ 0x3039

然后导出线程栈:

1
jstack <pid>

在里面搜索:

1
nid=0x3039

就可以找到这个高 CPU 线程当前正在执行什么代码。

整个流程就是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
top
→ 找到高 CPU Java 进程

top -Hp <pid>
→ 找到高 CPU 线程

线程 ID 转十六进制

jstack <pid>
→ 根据 nid 找线程栈

查看具体代码位置

常见的高 CPU 原因包括:

1
2
3
4
5
6
7
8
死循环
大量自旋
频繁 CAS 失败
大量正则计算
序列化 / 反序列化
加密压缩
GC 线程占用过高
线程竞争严重

如果发现占 CPU 最高的是 GC 线程,那么问题就不一定在某一行业务代码,而可能是 GC 太频繁。

这时候就需要再结合 GC 日志、堆使用情况和对象分配速度一起判断。

所以 CPU 飙高排查的核心思路是:

先定位进程,再定位线程,再用线程栈定位代码。

如果问题和 GC 有关,再回到 GC 日志和堆分析继续排查。