← 返回现象因果
💻 代码层的因与果
死循环 / 死递归 / 大对象 / 内存泄漏 —— 代码写法会直接引发什么现象
♻️死循环(while 无出口)CPU 100%
原因:while(true) 或循环条件永远为 true,线程在里面出不来了。
因果流程图
1
CPU 单核 100% (top -H 某个线程满载,但整体 CPU 可能不高因为其他核闲)
2
线程池被占满 (死循环线程占着不释放,新请求进不来 → 拒绝服务)
3
内存可能正常(不一定产生大对象)→ 但如果循环内创建对象 → 叠加内存涨 + Full GC
4
排查:jstack 看堆栈 → 直接定位到 while 循环的代码行
// ❌ 死循环(忘了加退出条件)
while (flag) {
processQueue(); // queue 永远不空,flag 永远 true
}
// ✅ 排查命令
// jstack <PID> | grep -A 30 " nid=0x"
// 看到线程一直停在某个 while 循环的行号
🌀死递归(没终止条件)StackOverflow
原因:递归函数没有 base case(终止条件),自己调自己无限深入。
因果流程图
1
直接抛 StackOverflowError (不是 OOM,是栈溢出!注意区别)
2
CPU 可能瞬间高(疯狂调用)→ 然后立即崩溃(比死循环快,栈很快就满)
4
排查:StackOverflow 的堆栈里会看到自己调自己 → 加终止条件 / 改迭代
// ❌ 死递归(没有 base case)
public int factorial(int n) {
return n * factorial(n - 1); // n=-1,-2... 永不终止
}
// ✅ 正确(有终止条件)
public int factorial(int n) {
if (n <= 1) return 1; // base case!
return n * factorial(n - 1);
}
和死循环的区别:死循环 → CPU 满但不死(一直转);死递归 → 快速崩溃(StackOverflow)。死循环可能导致线程池耗尽,死递归直接线程挂。
📦大对象(一次性加载太多)OOM
原因:一次性查询大量数据(50万行)或加载大文件,全放内存。
因果流程图
1
先 CPU 高(频繁 Full GC)→ GC 拼命回收但回收不掉(对象还在用)
2
然后 OOM 崩溃 (GC 放弃了,抛 OutOfMemoryError) → 服务挂
3
接口超时(GC STW 停顿让请求卡住)→ 用户看到卡 → 然后 500
4
排查:jmap dump → MAT 看 Dominator Tree → 找到那个 50 万行的 List
// ❌ 一次性全查
List<Order> all = orderDao.findAll(); // 50万行全进内存
exportExcel(all); // 堆撑爆 → OOM
// ✅ 分页 / 流式处理
// 1. 分页查:每次查1000条
for (int page = 0;; page++) {
List<Order> batch = orderDao.findPage(page, 1000);
if (batch.isEmpty()) break;
writeExcel(batch); // 写完这批就释放
}
// 2. EasyExcel SAX 流式写(内存恒定)
EasyExcel.write(file).sheet().doWrite(dataSupplier);
💧内存泄漏(只进不出)慢涨→OOM
原因:对象放进集合后不再删除(GC 还能找到它,所以不回收),越积越多。
因果流程图
1
内存阶梯式上涨 (监控看 Heap 曲线往上走不回落) → 越跑越慢
2
后期频繁 Full GC → CPU 偏高 + STW 停顿 → 最终 OOM
3
和大对象的区别:大对象是瞬时撑爆(查50万行);泄漏是慢慢涨(每天涨一点,几天才出问题)
4
为什么 static 会泄漏? static 变量是 GC Root(类不卸载就永久在),它引用的对象 GC 永远认为"还活着"。普通局部变量方法结束自动断 → 安全;static/ThreadLocal 永久连着 Root → 必须手动 remove
5
排查:jmap dump → MAT 看 Dominator Tree + Path to GC Roots → 找到谁引用着不释放
// ❌ 静态 Map 当缓存,只 put 不 remove
private static Map<String, User> cache = new HashMap<>();
public void login(User u) {
cache.put(u.getToken(), u); // 只进不出!
} // token 用完没人删,User 对象永远在 cache 里
// ✅ 方案1:用有上限+过期的缓存(Caffeine/Guava)
Cache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterAccess(30, MINUTES)
.build();
// ✅ 方案2:用 WeakHashMap(key 被回收时自动删)
// ✅ 方案3:用完显式 remove
常见泄漏源:① 静态集合只 add 不 remove ② ThreadLocal 没 remove(线程池复用)③ 监听器/定时器没注销 ④ 数据库连接没 close ⑤ 大对象被闭包持有(GC 可达就不回收)。
代码层因果总结:死循环烧CPU、死递归崩栈、大对象秒爆OOM、泄漏慢涨OOM。四种问题的现象不同,排查方向也不同。
现象因果 · 代码层 · 全栈资料库