🚨 救命!我的服务器要"爆炸"了!——Full GC频繁&CPU飙升救火指南 🔥

184 阅读12分钟

适合人群: Java后端工程师、运维小伙伴、想要进阶的编程萌新
难度等级: ⭐⭐⭐⭐ (中高级)
阅读时间: 15分钟
紧急程度: 🆘🆘🆘 (学会了就是救命技能!)


📖 引言:深夜两点的噩梦

想象一下这个场景:

深夜两点,你正在做美梦,梦见自己升职加薪当上总经理迎娶白富美走上人生巅峰...

滴滴滴! 📱

手机疯狂震动,钉钉消息、电话、短信接连不断:

  • "服务器CPU飙到90%了!"
  • "用户反馈系统卡死了!"
  • "老板问怎么回事!"

你猛地从床上坐起,冷汗直冒,心想:"完了,我的服务挂了!" 😱

别慌!今天这篇文章就是要教你如何像一个消防员一样快速灭火,像一个侦探一样精准定位问题!


🎯 第一章:理解"案发现场"——什么是Full GC?

1.1 垃圾回收的生活比喻 🗑️

首先,我们需要理解Java的垃圾回收机制。

想象你家里有一个房子(这就是Java堆内存),房子分为三个房间:

  1. 婴儿房(Eden区) 👶 - 新生的对象都住这里,超级拥挤
  2. 儿童房(Survivor区) 🧒 - 婴儿长大了,搬到这里
  3. 成人房(老年代) 👨 - 活得够久的"老对象"最终住这里

每天家里都会产生垃圾(不再使用的对象),需要定期清理:

  • Minor GC(小清理) 🧹 - 只清理婴儿房和儿童房,速度快,影响小
  • Major GC(中清理) 🧽 - 清理成人房,耗时较长
  • Full GC(大扫除) 🚛 - 全屋大清理!整个房子都要翻个底朝天!

1.2 Full GC为什么这么可怕?

Full GC就像全家总动员大扫除:

  • ⏸️ 所有人都要停下手中的活(Stop The World - STW)
  • 耗时超级长(可能几百毫秒到几秒)
  • 😰 频繁发生的话,整个家都没法正常生活了(用户感觉系统卡死)

当Full GC频繁发生时,就像你家每10分钟就要来一次全屋大扫除,谁受得了?!


🔍 第二章:案件分析——为什么会Full GC频繁 + CPU飙升?

2.1 常见"犯罪嫌疑人" 🕵️

嫌疑人作案手法生活比喻
内存泄漏 💦对象用完了不释放,越积越多你买了一堆东西从不扔,家里堆满了破烂
内存溢出 💥瞬间创建大量对象,内存不够双11疯狂购物,房子瞬间塞满
大对象 🐘创建超大对象直接进老年代买了一张超大床,直接占满成人房
元空间爆满 📚动态加载太多类图书馆的书太多,书架爆了
代码Bug 🐛死循环创建对象生产线失控,疯狂生产产品
GC参数不合理 ⚙️堆内存太小,GC触发频繁房子太小,稍微放点东西就要清理

2.2 CPU飙升的"同伙" 🤝

Full GC频繁导致CPU飙升的原因:

  1. GC线程疯狂工作 - 就像清洁工不停地扫地,累到虚脱
  2. 应用线程频繁暂停和恢复 - 像你工作时每分钟被打断一次,效率极低
  3. 对象标记和复制消耗大量CPU - 搬家公司搬运家具累死累活

🚑 第三章:急救手册——快速定位问题的5步法

🔥 紧急情况!先看当前状态!

Step 1: 查看JVM进程和CPU占用 🎯

# 找出Java进程ID
top

# 或者更精确
ps -ef | grep java

# 看到类似这样的输出:
# PID    USER   CPU%  MEM%  COMMAND
# 12345  root   90.0  45.0  java -jar myapp.jar

生活化理解: 就像你要知道是哪个房间在冒烟,才能精准灭火!

关键指标:

  • CPU超过70%就要警惕了⚠️
  • CPU持续90%以上就是紧急情况🆘

Step 2: 使用jstat查看GC情况 📊

# 假设进程ID是12345
jstat -gc 12345 1000 10

# 每1000毫秒(1秒)打印一次,共打印10次

输出示例:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    YGC   YGCT    FGC    FGCT     GCT   
10752  10752  0      8746   86528    54321    175104    174000   52864  51234  258   2.034   15    12.567   14.601

看懂这些"天书": 🔮

列名含义看什么
FGCFull GC次数❗ 如果快速增长,说明Full GC频繁!
FGCTFull GC总耗时(秒)❗ 占总时间比例高,说明系统大部分时间在GC!
OU老年代已使用❗ 接近OC(老年代容量),说明老年代快满了!
YGCYoung GC次数正常情况,次数多但耗时短

🚨 报警线:

  • Full GC频率 > 每分钟1次 → 危险!
  • Full GC耗时占比 > 10% → 严重!
  • 老年代使用率 > 90% → 爆炸边缘!

Step 3: 使用jmap导出堆转储文件 💾

# 导出堆快照(Heap Dump)
jmap -dump:live,format=b,file=/tmp/heap_dump.hprof 12345

# 参数解释:
# -dump:live  - 只导出存活对象
# format=b    - 二进制格式
# file=xxx    - 保存路径

⏰ 注意: 这个操作会触发一次Full GC,生产环境谨慎使用!

生活化理解: 就像给房子拍一张全景照片,记录下所有物品的位置,方便后续分析。

文件大小参考:

  • 4G堆内存 → 文件约1-2G
  • 8G堆内存 → 文件约2-4G

Step 4: 使用jstack查看线程状态 🧵

# 导出线程快照
jstack 12345 > /tmp/thread_dump.txt

# 多次导出(间隔3秒)以便对比
jstack 12345 > /tmp/thread_dump_1.txt
sleep 3
jstack 12345 > /tmp/thread_dump_2.txt
sleep 3
jstack 12345 > /tmp/thread_dump_3.txt

看什么:

  1. 死锁(Deadlock) 🔒
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8e1c004e00
  which is held by "Thread-2"
  1. BLOCKED状态的线程 🚧
"http-nio-8080-exec-25" #50 daemon prio=5 os_prio=0 tid=0x00007f8e1c123456 nid=0x7f8e waiting for monitor entry
   java.lang.Thread.State: BLOCKED (on object monitor)
  1. CPU占用高的线程 🔥
# 1. 找出CPU占用高的线程
top -Hp 12345

# 2. 看到线程ID,比如 12567
# 3. 转换为16进制
printf "%x\n" 12567
# 输出:3117

# 4. 在thread dump中搜索 nid=0x3117

Step 5: 查看GC日志 📝

先确保开启了GC日志:

# JVM启动参数(JDK 8)
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-XX:+PrintGCTimeStamps 
-Xloggc:/var/log/gc.log

# JDK 9+
-Xlog:gc*:file=/var/log/gc.log:time,level,tags

GC日志示例:

2025-10-21T14:23:45.123+0800: 1234.567: [Full GC (Allocation Failure) 
[PSYoungGen: 2048K->0K(2560K)] 
[ParOldGen: 7000K->6900K(7168K)] 
9048K->6900K(9728K), 
[Metaspace: 3456K->3456K(1056768K)], 
2.3456789 secs]

解读: 😰

  • Full GC (Allocation Failure) - 分配内存失败触发的Full GC
  • 2.3456789 secs - 耗时2.3秒!用户感觉系统卡死了!
  • 7000K->6900K - 老年代几乎没回收掉对象,说明有内存泄漏!

🧪 第四章:深度分析——用MAT分析堆转储文件

4.1 安装MAT工具 🛠️

MAT (Memory Analyzer Tool) - 内存分析神器!

下载地址:www.eclipse.org/mat/downloa…

生活化理解: MAT就像一个超级侦探,能帮你从一堆杂乱的物品中找出"真凶"!

4.2 打开堆转储文件 📂

  1. 启动MAT
  2. File → Open Heap Dump
  3. 选择之前导出的 heap_dump.hprof
  4. 选择 Leak Suspects Report(泄漏嫌疑报告)

4.3 看懂MAT的核心功能 🎯

🔹 Histogram(直方图)- 看对象数量

显示每个类的实例数量和占用内存:

Class Name                    | Objects | Shallow Heap | Retained Heap
------------------------------|---------|--------------|---------------
char[]                        | 1234567 | 50 MB        | 50 MB
java.lang.String              | 1000000 | 24 MB        | 74 MB
com.example.User              | 500000  | 100 MB       | 200 MB  ⚠️

🚨 异常信号:

  • 某个业务对象数量特别多(比如User对象有50万个)
  • 数组、String、Map数量异常巨大

🔹 Dominator Tree(支配树)- 看谁占内存最多

显示哪些对象占用内存最多:

Object                        | Shallow Heap | Retained Heap
------------------------------|--------------|---------------
com.example.CacheManager      | 64 bytes     | 500 MB  💥
  ├─ HashMap                  | 128 bytes    | 499 MB
  │   ├─ Entry[]              | 4 MB         | 495 MB
  │   └─ Entry<K,V>           | 32 bytes     | 200 MB

生活化理解: 就像找出家里最占地方的东西,可能是那个从不穿的大衣柜!

🔹 Leak Suspects(泄漏嫌疑)- 自动找问题

MAT会自动分析并列出可疑的内存泄漏:

Problem Suspect 1:

One instance of "com.example.CacheManager" loaded by "sun.misc.Launcher$AppClassLoader @ 0x12345678"
occupies 520,123,456 bytes (85% of total heap).

The memory is accumulated in one instance of "java.util.HashMap", 
loaded by "<system class loader>", which occupies 519,000,000 bytes.

Keywords: java.util.HashMap, com.example.CacheManager

翻译: 你的CacheManager里有个HashMap占了85%的堆内存,大哥你这是要上天啊!🚀


🎓 第五章:常见案例与解决方案

案例1:内存泄漏 - ThreadLocal没清理 💧

症状:

  • Full GC频繁但回收不掉内存
  • 老年代使用率持续上升
  • 最终OutOfMemoryError

代码问题:

// ❌ 错误示例
public class UserContext {
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userThreadLocal.set(user);
        // 忘记remove了!
    }
}

// 在线程池中使用
threadPool.execute(() -> {
    UserContext.setUser(currentUser);
    // 业务逻辑
    // 线程复用,但ThreadLocal没清理,内存泄漏!
});

✅ 解决方案:

public class UserContext {
    private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    
    public static void setUser(User user) {
        userThreadLocal.set(user);
    }
    
    public static void clearUser() {
        userThreadLocal.remove();  // ✅ 一定要清理!
    }
}

threadPool.execute(() -> {
    try {
        UserContext.setUser(currentUser);
        // 业务逻辑
    } finally {
        UserContext.clearUser();  // ✅ 在finally中清理
    }
});

案例2:大对象分配 - 一次性加载太多数据 🐘

症状:

  • Full GC突然频繁
  • 每次GC后内存会下降,但很快又满了

代码问题:

// ❌ 错误示例:一次性查询100万条数据
public List<Order> getAllOrders() {
    return orderMapper.selectAll();  // 💥 100万条数据直接加载到内存
}

✅ 解决方案:

// ✅ 分页查询
public void processAllOrders() {
    int pageSize = 1000;
    int pageNum = 1;
    
    while (true) {
        List<Order> orders = orderMapper.selectByPage(pageNum, pageSize);
        if (orders.isEmpty()) {
            break;
        }
        
        // 处理这一页数据
        processOrders(orders);
        
        // 帮助GC
        orders.clear();
        orders = null;
        
        pageNum++;
    }
}

案例3:缓存没有过期策略 ⏰

症状:

  • 内存使用率持续上升
  • 缓存对象在堆中占大量内存

代码问题:

// ❌ 错误示例:永不过期的缓存
public class UserCache {
    private static Map<Long, User> cache = new ConcurrentHashMap<>();
    
    public static User getUser(Long id) {
        return cache.computeIfAbsent(id, k -> {
            return userService.getUserById(k);  // 一直往缓存加,从不清理
        });
    }
}

✅ 解决方案:

// ✅ 使用Guava Cache带过期时间
public class UserCache {
    private static LoadingCache<Long, User> cache = CacheBuilder.newBuilder()
        .maximumSize(10000)  // 最多缓存1万个
        .expireAfterWrite(30, TimeUnit.MINUTES)  // 30分钟过期
        .build(new CacheLoader<Long, User>() {
            @Override
            public User load(Long key) {
                return userService.getUserById(key);
            }
        });
    
    public static User getUser(Long id) {
        return cache.getUnchecked(id);
    }
}

案例4:GC参数设置不合理 ⚙️

症状:

  • 堆内存设置太小
  • 新生代和老年代比例不合理

问题配置:

# ❌ 错误示例:堆内存太小
java -Xms512m -Xmx512m -jar myapp.jar
# 应用实际需要2G,只给512M,频繁Full GC

✅ 解决方案:

# ✅ 合理的配置(假设机器8G内存)
java -Xms4g -Xmx4g \
  -XX:NewRatio=2 \
  -XX:SurvivorRatio=8 \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:+PrintGCDetails \
  -XX:+PrintGCDateStamps \
  -Xloggc:/var/log/gc.log \
  -jar myapp.jar

# 参数说明:
# -Xms4g -Xmx4g          : 初始和最大堆都是4G(避免动态扩展)
# -XX:NewRatio=2         : 新生代:老年代 = 1:2
# -XX:SurvivorRatio=8    : Eden:Survivor = 8:1
# -XX:+UseG1GC           : 使用G1垃圾回收器(推荐)
# -XX:MaxGCPauseMillis   : GC最大停顿时间200ms

📋 第六章:完整的问题排查清单

🔥 应急响应流程(10分钟内)

□ 1. 确认问题:CPU、内存、GC频率
    ├─ top 命令查看CPU
    ├─ jstat -gc 查看GC
    └─ 确认是否真的是Full GC问题

□ 2. 保留现场证据
    ├─ jmap -dump 导出堆快照
    ├─ jstack 导出线程快照  
    └─ 复制GC日志

□ 3. 临时止血措施
    ├─ 重启应用(快速恢复服务)
    ├─ 限流降级(减少负载)
    └─ 扩容(临时方案)

□ 4. 深度分析(后续)
    ├─ MAT分析堆转储
    ├─ 找出内存泄漏点
    └─ 修复代码

□ 5. 预防措施
    ├─ 调整JVM参数
    ├─ 增加监控告警
    └─ 压测验证

🛠️ 常用命令速查表

# 1️⃣ 查看Java进程
jps -v

# 2️⃣ 查看GC情况(每秒刷新)
jstat -gc <pid> 1000

# 3️⃣ 查看GC统计
jstat -gcutil <pid> 1000 10

# 4️⃣ 导出堆快照
jmap -dump:live,format=b,file=heap.hprof <pid>

# 5️⃣ 查看堆内存概要
jmap -heap <pid>

# 6️⃣ 导出线程快照
jstack <pid> > thread.txt

# 7️⃣ 查看JVM参数
jinfo -flags <pid>

# 8️⃣ 动态修改JVM参数(部分支持)
jinfo -flag +PrintGCDetails <pid>

🎯 第七章:预防胜于治疗 - 监控告警

监控指标 📊

指标阈值告警级别
CPU使用率>70%⚠️ 警告
CPU使用率>85%🆘 严重
Full GC频率>1次/分钟🆘 严重
Full GC耗时>1秒⚠️ 警告
老年代使用率>80%⚠️ 警告
老年代使用率>90%🆘 严重
堆内存使用率>85%⚠️ 警告

监控工具推荐 🔧

  1. Prometheus + Grafana - 开源监控组合拳
  2. Skywalking - APM应用性能监控
  3. Arthas - 阿里开源的Java诊断工具
  4. JVM监控面板 - 可视化GC、内存、线程

💡 总结:关键要点速记

🎓 核心知识点

  1. Full GC = 全屋大扫除,会STW(Stop The World),影响用户体验
  2. 频繁Full GC的三大原因:内存泄漏、内存溢出、参数不合理
  3. 救火四件套:jstat、jmap、jstack、MAT

🚀 最佳实践

代码层面:

  • ThreadLocal用完必须remove
  • 避免一次性加载大量数据
  • 缓存必须设置过期时间
  • 大对象能分批就分批

JVM配置:

  • 堆内存设置合理(机器内存的50-70%)
  • 使用G1 GC(推荐)
  • 开启GC日志
  • 监控告警要到位

应急响应:

  • 保留现场(dump文件)
  • 先止血(重启/限流)
  • 再治本(修代码)
  • 最后预防(加监控)

🎉 结语

恭喜你!🎊 读完这篇文章,你已经掌握了:

  • ✅ Full GC的原理和危害
  • ✅ 快速定位问题的5步法
  • ✅ 使用MAT分析内存问题
  • ✅ 常见案例和解决方案
  • ✅ 监控预防措施

下次再遇到线上Full GC问题,你就不会慌了!记住:

"临危不乱,先保现场,再找原因,最后优化!" 🚀


📚 扩展阅读

  • 《深入理解Java虚拟机》- 周志明
  • 《Java性能优化权威指南》
  • Oracle官方JVM调优指南

💪 加油,Java工程师!愿你的服务永不Full GC! 😄


声明: 本文档基于Java 8-17版本编写,部分工具和参数可能因版本而异。
最后更新: 2025年10月
作者: AI助手(用❤️和☕创作)