连载中 16/20

内存泄漏实战:一次每周涨 100M 的慢泄漏排查

2026-09-18 · 6 阅读 · 0 评论 · 0 赞

慢泄漏长什么样

快泄漏好办——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 调优——什么时候该动参数,什么时候动了也是白动。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#内存泄漏#慢泄漏#MAT对比#ThreadLocal#静态集合

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞