路径搜索是逆向追踪对象到gc root的机制,揭示“为何该对象未被回收”;对全局闭包,它唯一暴露谁在长期持有——需用“with all references”并检查context字段,而非依赖支配树。

什么是“路径搜索”在堆快照里的真实作用
它不是泛泛地查引用,而是从一个选定对象出发,逆向追踪到最近的 GC Root,告诉你“为什么这个对象没被回收”。对全局闭包(比如被 window、globalThis、静态变量或长生命周期单例持有的函数/对象)来说,这就是唯一能暴露“谁在偷偷留着它”的机制。
Path to GC Roots 在 MAT 中怎么用才不漏掉闭包链
MAT 的 Path to GC Roots 默认只显示强引用路径,而闭包常通过隐式引用(如函数词法环境中的 [[Environment]])存活——这类引用在快照里表现为 java.lang.Class 或 java.lang.reflect.Method 持有的 ConstantPool,或 JS 堆中 Closure 类型对象的 context 字段。操作时必须注意:
- 右键目标对象后,选
Path to GC Roots → with all references,不能只选with outgoing references; - 勾选
exclude weak/soft/phantom references—— 闭包泄漏几乎全是强引用导致的,弱引用路径会干扰判断; - 若路径终点是
ClassLoader或ThreadLocal,立刻检查对应类是否缓存了闭包(比如 Spring 的@EventListener方法被注册进静态监听器容器); - 在 JS 堆快照(Chrome DevTools)中,要展开
Closure节点下的context → Closure层级,才能看到被捕获的外部变量实际指向哪个对象。
为什么“支配树(Dominator Tree)”容易误导闭包分析
Dominator Tree 显示的是“删掉该对象后能释放多少 Retained Heap”,但它对闭包极不敏感:一个闭包可能只占几 KB,但它的存在让整个数据模块(比如一个 50MB 的缓存 Map)无法回收。这时它在支配树里排名很低,反而会被忽略。真正该盯的是:
- 在
Histogram中筛选java.lang.Object或自定义类,按Retained Heap排序,找“实例数少但 Retained Heap 异常高”的项; - 对这些对象逐个执行
Path to GC Roots,重点看路径中是否含static字段、final单例、或Thread实例; - 若路径终点是
java.lang.Thread,检查该线程是否还在运行且持有Runnable闭包(常见于未 shutdown 的 ScheduledExecutorService)。
JS 堆快照里识别全局闭包的三个关键特征
Chrome DevTools 的堆快照对闭包更直接,但需主动识别模式:
- 类型为
Closure的对象,其name字段为空或为匿名函数名,且size明显大于同级普通对象; - 展开其
context后,出现大量本不该长期存在的数据引用(比如 DOM 节点、大型数组),而这些数据本应在页面跳转后销毁; - 在
Retainers视图中,该Closure被window、document或某个长期存活的模块对象(如AppStore)直接引用 —— 这就是死内存的物理锚点。
闭包本身不占大内存,问题永远出在它捕获的内容和它被谁持有着。路径搜索不是终点,而是把“谁在 hold 它”这层纸捅破的动作。漏掉一次 with all references,就可能让一个跨页面的内存泄漏藏上半年。










