慢泄漏长什么样
快泄漏好办——OOM 一响现场还在。麻烦的是慢泄漏:每周老年代基线涨 100M,Full GC 能清但清不回原点,几周后终于压垮。它在监控曲线上的签名很典型:锯齿的底部不断抬高——每次 GC 后的"地板"比上次高一点。这不是 GC 问题,是有一批对象被谁拴住了不让走。
排查四步
// 1. 确认现象:jstat 看老年代基线是否持续抬升
$ jstat -gcutil PID 5000 // 隔天再看:O 列的"谷底"在涨
// 2. 抓两份 dump,间隔一天(低峰期 jmap 或 OOM 自动 dump)
$ jmap -dump:live,format=b,file=day1.hprof PID
$ jmap -dump:live,format=b,file=day2.hprof PID
// 3. MAT 对比:Histogram 里按对象数排序,day2 比 day1 多出的
// 大户,就是泄漏嫌疑——比如 byte[] 多了 80 万个
// 4. Path to GC Roots(exclude weak)找持有者
// 引用链顶端往往是个静态字段——这就是"拴住"的那只手三大窝点
一,静态集合只进不出:static Map<String, Object> cache 当本地缓存用,只 put 不淘汰——静态变量是 GC Root,集合里所有对象永远可达。修法是换 Caffeine 这类带淘汰策略的缓存,或 LinkedHashMap 的 removeEldestEntry。二,ThreadLocal 不 remove:线程池线程长生不老,ThreadLocalMap 里 value 是强引用,每处理一个请求就攒一条——前面第 6 篇讲过机制,这里是它的实战形态。三,监听器/回调注册后不注销:往长生命周期的对象(Spring 容器、连接池、事件总线)上注册了匿名回调,忘了 unregister,引用链就永久成立。
类加载器泄漏:另一种隐形账
热部署、动态脚本(Groovy、JSP 重编译)场景下,Metaspace 缓慢增长也是泄漏——每次热部署都生成一个新的类加载器,旧加载器只要被一个对象引用着就卸不掉,里面所有类全部陪葬。MAT 里表现为同名类出现几十份、ClassLoader 对象一大串。修法是确保旧 ClassLoader 没有残留引用,必要时重启兜底。
预防比排查便宜
三条军规:缓存必须有界(不许裸 Map 当缓存);ThreadLocal 必须 try/finally remove;注册监听器的类必须提供对称的注销路径并接入生命周期(@PreDestroy)。再加一道监控保险:对老年代 GC 后的谷底值设告警(比如连续三天抬高 5% 就通知),把发现时机从"几周后的 OOM"提前到"第二天的异常曲线"。下一篇聊 JVM 调优——什么时候该动参数,什么时候动了也是白动。
评论 (0)