← 返回现象因果

💻 代码层的因与果

死循环 / 死递归 / 大对象 / 内存泄漏 —— 代码写法会直接引发什么现象

♻️死循环(while 无出口)CPU 100%
原因:while(true) 或循环条件永远为 true,线程在里面出不来了。
因果流程图
while(true) 无出口 单线程 CPU 占满 其他请求排队 服务假死
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(终止条件),自己调自己无限深入。
因果流程图
递归无终止 栈帧不断叠加 超出栈深度 StackOverflowError线程崩溃
1
直接抛 StackOverflowError (不是 OOM,是栈溢出!注意区别)
2
CPU 可能瞬间高(疯狂调用)→ 然后立即崩溃(比死循环快,栈很快就满)
3
内存不涨 (栈帧随崩溃释放,不会导致 OOM)
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万行)或加载大文件,全放内存。
因果流程图
查50万行全放List 堆瞬间撑大 FullGC 救场失败 OOM 崩溃服务挂
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 还能找到它,所以不回收),越积越多。
因果流程图
静态Map只add不remove 对象永不回收 内存阶梯式涨 几天后OOM
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。四种问题的现象不同,排查方向也不同。
现象因果 · 代码层 · 全栈资料库