支配树用于精准定位内存泄漏根因:默认按retained heap降序排列,优先分析前3–5项中业务包名类节点;右键查path to gc roots(排除弱/软/虚引用)获取强引用链;结合dominator depth≤3且retained heap>100mb、实例数突增及首个业务类位置交叉验证。

支配树不是用来“通读”的,而是用来“狙击”的——盯住Retained Heap最大的几个节点,逆向查强引用链,再结合业务逻辑确认是否真泄漏。
看排序:Retained Heap降序是核心线索
打开MAT的Dominator Tree视图,默认按“Retained Heap”从大到小排列。这个值代表:如果该对象被回收,整个子树(所有被它支配的对象)都会被释放,对应的真实内存大小。
- 排第一的不一定是泄漏源,但它是当前堆中“内存控制力最强”的节点——比如一个静态缓存Map、Spring的ApplicationContext、或某个未清理的ThreadLocalMap
- 重点关注前3–5项,尤其是业务包名开头的类(如com.yourcompany.cache.UserSessionCache),而非java.util.HashMap或byte[]这类底层容器
- 若看到大量相同数组(如char[]、byte[])扎堆,别点进去;先右键→“Immediate Dominators”,看是谁在统一持有它们
看路径:Path to GC Roots只留强引用
在支配树中选中可疑节点,右键→“Path to GC Roots” → 勾选“exclude weak/soft/phantom references”。这一步过滤掉所有不影响GC的引用,只保留真正阻止回收的强引用链。
- 典型泄漏路径包括:ThreadLocalMap → ThreadLocal → 业务对象 → 大数组,或StaticCache → HashMap → Key → Value → List → 元素对象
- 路径中若出现java.lang.Thread.threadLocals,且线程来自线程池(如pool-1-thread-3),要高度警惕ThreadLocal泄漏
- 路径过长时,可点击中间某层对象(如某个Service实例),再对它单独查支配树——真正的根因常藏在第二层
看深度与上下文:Dominator Depth + 时间线交叉验证
MAT中右键节点→“Show in Overview”,查看“Dominator Depth”。深度≤3且Retained Heap > 100MB的节点,优先级最高。
- 深度小 = 距离GC Roots近 = 控制范围广,更可能是源头
- 单靠快照不够,要对比两次jmap -histo输出:若某类实例数1小时内涨了4倍,又恰好落在高Retained Heap节点下方,可信度大幅提升
- 顺着支配路径往上找,第一个出现的业务包名类(如com.yourcompany.order.OrderProcessor),基本就是泄漏代码所在位置,重点检查它的静态字段、单例缓存、监听器注册、回调未注销等模式
防误判:识别常见干扰项
框架类和基础结构本身合法,但容易成为“中转站”,不能见Retained Heap大就认定是泄漏。
- 排除java.*、sun.*、javax.*下的原生类(除非你改了JVM)
- Spring的GenericApplicationContext、Tomcat的WebappClassLoader、线程池ThreadPoolExecutor本身长期存活,需进一步查它里面是否堆积了不该存在的业务对象
- 注意[Synthetic]标记的虚拟节点——它可能代表整个线程栈或ClassLoader,真实泄漏源往往藏在它展开后的子节点里











