浅堆与深堆:不只是"占多大"
MAT 里有两种"大小":浅堆(Shallow Heap)是对象自身占用的内存——一个 ArrayList 的浅堆只有几十字节(对象头 + 内部数组引用 + size 字段),并不算它装着的元素;深堆(Retained Heap)则是"这个对象被回收后能释放多少内存"——它的全部独占对象,包括内部数组和能跟着它一起死的对象。排查泄漏看深堆,理解对象本身开销看浅堆。
三步排查法
// 步骤一:看 Leak Suspects 报告(MAT 自动分析)
// 打开 hprof → MAT 自动跑一轮,列出嫌疑对象
// 常见结论:某 HashMap 占了 800M / 总堆 1G —— 直接点名
//
// 步骤二:Dominator Tree(支配树)
// 按深堆排序,最上面就是"拴住最多内存"的根
// 树状展开:父节点回收,子节点全部跟着死——谁是"真凶"一眼看出
//
// 步骤三:Path to GC Roots(强引用链)
// 选中对象 → List Objects → with incoming references →
// 选 "Path To GC Roots" → exclude weak/soft references
// 看到的引用链就是"谁拴住了它不让回收"一个真实案例:ThreadLocal 造成的泄漏
某服务每隔几天 OOM 一次,堆转储里 70% 内存是 com.xxx.UserContext 的实例——一堆 UserContext 没被回收。Path to GC Roots 顺着强引用链摸上去:UserContext → ThreadLocalMap.Entry → Thread → 线程池里的 Worker。真凶是业务代码漏了 ThreadLocal.remove()——线程池的线程长生不老,ThreadLocalMap 里 key 虽是弱引用,value 却是强引用,每跑一个用户就攒一条 stale entry。
SQL 结果集泄漏:另一类高发
堆转储里 PreparedStatement$1、ResultSetImpl 一堆——这是 JDBC 资源没关。MySQL Connector/J 的 ResultSet 内部持有查询结果 byte[],不主动 close 就一直挂在连接对象上,连接还回池子也不会释放。MyBatis/正常 ORM 几乎不会出这事,但手写 JDBC、用 ResultSet 做缓存的场景,这种泄漏常见。MAT 里这类对象的引用链往往指向 Connection → Pool,是排查泄漏的高频路径。
实践要点
第一,dump 时机决定排查成败:最好在 OOM 前夕或内存刚涨起来时 dump,事后重启再 dump 可能问题已自愈看不到现场。配 HeapDumpBeforeFullGC 让 Full GC 前自动 dump 是最省心的。第二,转储文件别在生产分析:动辄几个 G,拷到本地用 MAT 分析更安全。第三,排除弱软引用看强引用链:默认 Path to GC Roots 包含所有引用,过滤后才能看真凶。下一篇把各种 OOM 的"出生地"做个地图。
评论 (0)