← 返回思维链目录

Java 与 JVM 思维链

看到 OOM、GC、线程安全、类加载问题,顺藤摸瓜找到根因

1. OOM:内存怎么就溢出了

现象java.lang.OutOfMemoryError,服务挂了重启
本质:JVM 要分配内存但堆装不下了,GC 也回收不出足够空间。要么瞬时塞太多(大对象),要么只进不出(泄漏),要么堆参数太小。
直觉:先分清"突然爆还是慢涨" —— 突然爆看哪里一次性加载大量数据,慢涨找引用链(谁引用着不释放)。
🌊 顺藤摸瓜
① 找大对象
jmap -dump 导堆快照 → MAT 看 Dominator Tree → 按大小排序,第一个就是罪魁
jmap dumpMAT 支配树对象实例数
② 大对象?
瞬时撑爆 → 一次性查 50 万行 / 大 Excel / 大文件全量读 → 改分页 / 流式(EasyExcel SAX)
查询没分页EasyExcel 流式游标查询
③ 泄漏?
只增不减 → 找 GC Root 引用链(MAT 的 Path to GC Roots)→ 常见:ThreadLocal 没 remove / 静态集合只 add / 监听器没注销
GC Root 引用链ThreadLocal 没 remove静态集合增长
💡 为什么 static/ThreadLocal 最危险?
GC Roots = JVM 认为一定还活着的东西。① 栈局部变量 → 方法结束自动断(安全) ② static 变量 → 类不卸载就永久连着 Root(极危险) ③ 活跃线程 → 线程池不销毁,ThreadLocalMap 跟着不死(极危险) ④ 同步锁/常量/JNI → 较少见
局部变量=自动断static=永久RootThreadLocal=线程不死就不回收普通成员变量=跟外层对象走
④ 不是 heap?
看具体报错:Metaspace(动态代理生成类太多,CGLIB)/ Direct memory(NIO)/ unable to create thread(线程数到上限)/ StackOverflow(死递归)
MetaspaceDirect memory线程数上限
⑤ OOM 前兆
OOM 前 GC 拼命救场 → 所以 OOM 前往往先 CPU 高(频繁 Full GC) → 配 HeapDumpOnOutOfMemoryError 自动留现场
先 CPU 高后 OOM自动 dump 参数
三大原因:大对象(瞬时撑爆)/ 泄漏(慢涨)/ 堆太小(参数)。先分清是哪种。

2. GC 频繁 / STW 卡顿

现象接口周期性卡顿,Full GC 频繁,RT 抖动
本质:GC 清垃圾腾空间。频繁 = 垃圾产生太快 / 回收效率低 / 空间太小。每次 GC 要暂停应用(STW),用户就感觉卡。
🌊 顺藤摸瓜
① 看 GC 频率
jstat -gcutil 看 YGC/FGC → 哪个涨得快就是哪个问题
jstat -gcutilYGC vs FGC
② YGC 频繁
年轻代 GC 频繁 = 对象产生快 → 看哪里狂 new:循环内 new / 大 JSON 反序列化 / 日志对象 / 自动装箱
循环内创建JSON 反序列化自动装箱
③ FGC 频繁
Full GC 频繁 = 老年代满 → 大对象直接进老年代 / Survivor 太小提前晋升 / 泄漏不回收
大对象进老年代Survivor 太小老年代泄漏
④ STW 长
单次停顿久 = 收集器不对 → 换 G1(分 Region 可预测停顿)/ ZGC(亚毫秒) → 或堆太大单次回收久
G1 可预测停顿ZGC 低延迟吞吐 vs 延迟
先问三个:堆够不够大?对象产生快不快?有没有泄漏?前两个调参数,第三个找泄漏对象。

3. 线程安全:为什么并发会出错

现象单测没问题,上线后偶发数据错乱、计数少、超卖
本质:多线程同时读写共享变量,"读-改-写"不是原子操作,中间会被打断,导致丢失更新或脏读。本质是竞态条件
🌊 顺藤摸瓜
① 竞态条件
count++ 不是原子(读-改-写三步)→ 线程 A 读到 5,还没写,B 也读到 5,都写 6 → 丢了一次 → 解决:加锁 / 用原子类
竞态条件读-改-写非原子丢失更新
② 怎么解决
synchronized / Lock(加锁串行);② AtomicInteger(CAS 无锁);③ ThreadLocal(每线程一份,不共享);④ 并发容器(ConcurrentHashMap/CopyOnWriteArrayList)
synchronizedAtomicInteger(CAS)ThreadLocalConcurrentHashMap
③ volatile
volatile 保证可见性(改了其他线程立刻看到)+ 禁止重排序 → 但不保证原子性(i++ 还是不安全)→ 适合状态标志位
可见性禁止重排序不保证原子
④ ThreadLocal 泄漏
ThreadLocal 用完不 remove → 线程池复用线程 → 串数据(读到上个任务的值)+ 泄漏(value 强引用)→ finally 必须 remove
ThreadLocal 没 remove线程池串数据finally remove
思路:并发问题 = 共享 + 非原子。要么加锁(共享串行),要么不共享(ThreadLocal),要么用原子类(CAS)。

4. 类加载:为什么找不到类

现象ClassNotFoundException / NoClassDefFoundError / 类冲突
本质:JVM 用双亲委派机制加载类(先让父加载器加载)。找不到类通常是依赖冲突、版本不对、或打包漏了。
🌊 顺藤摸瓜
① 双亲委派
收到加载请求先委派给父加载器(引导→扩展→应用)→ 父加载不到自己才加载 → 保证核心类不被篡改(自己写个 java.lang.String 也没用)
双亲委派引导/扩展/应用防篡改
② 找不到类
ClassNotFoundException → ① 依赖没引入(pom 缺);② 打包没打进去(scopeprovided);③ 版本冲突(同包多版本)→ mvn dependency:tree
依赖缺失scope=provided版本冲突
③ 打破委派
什么时候打破双亲委派? → Tomcat(每个 webapp 独立类加载器隔离应用)/ SPI(JDBC Driver)/ 热部署(OSGi)
Tomcat 隔离SPI热部署

5. Spring:@Transactional 失效等坑

现象加了 @Transactional 但事务没生效,数据没回滚
本质:Spring 事务基于 AOP 动态代理。通过代理对象调用才生效,直接 this 调用绕过代理,事务就不生效。
🌊 顺藤摸瓜
① 失效场景
同类内部 this 调用(绕过代理);② 方法不是 public;③ 异常被 catch 了没抛出;④ rollbackFor 没配(默认只回滚 RuntimeException);⑤ 类不是 Spring Bean(自己 new 的)
this 调用绕过代理非 public异常被吞rollbackFor
② AOP 原理
Spring 给 Bean 创建代理对象(JDK 动态代理 / CGLIB)→ 调用时代理拦截,先开事务→执行→提交/回滚 → 所以必须通过代理对象调用
JDK 动态代理CGLIB代理拦截
③ IoC/DI
为什么有 IoC? → 对象创建交给 Spring 容器(不用自己 new)→ DI 自动注入依赖 → 解耦(不用关心对象怎么来)→ 循环依赖用三级缓存解决
IoC 控制反转DI 依赖注入三级缓存

总结

现象本质第一反应
OOM堆装不下了突然爆(大对象)还是慢涨(泄漏),jmap+MAT
GC 频繁垃圾产生快/回收不掉/空间小jstat 看 YGC/FGC,调堆/优化对象/找泄漏
并发出错共享+非原子=竞态加锁/原子类/ThreadLocal/并发容器
找不到类双亲委派加载失败依赖缺失/版本冲突/打包漏了
事务失效AOP 代理被绕过this 调用/非 public/异常被吞
本质:Java 问题的核心大多在"内存"(OOM/GC)和"并发"(线程安全)。内存看分配与回收,并发看共享与原子。