保留大小(Retained Size)指删除该对象后GC可回收的内存总量,反映其真实内存占用;MAT中需关闭“Keep unreachable objects”并使用支配树分析,结合GC Roots路径与业务逻辑判断是否泄漏。
保留大小不是对象自身占多少字节,而是「删掉它,能顺带让 GC 回收多少内存」。比如一个 HashMap 实例的 retained size 很大,往往意味着它持有的键值对、内部数组、以及这些值又引用的下游对象(如 String、ArrayList)全都没法被回收——这才是真正卡住内存的“根因”。浅层大小(shallow size)只算对象头和字段指针,容易误判;深层大小(deep size)把所有引用对象都算进来,但包含可能被其他路径强引用的对象,干扰定位。
默认打开这个选项会导致分析结果里混入大量已不可达、本该被 GC 却还没清理的对象(比如刚被置为 null 但 GC 尚未运行),它们的 retained size 是 0 或极小,却会挤占列表顶部,掩盖真正的问题对象。操作路径是:Preferences → Memory Analyzer → Keep unreachable objects → 取消勾选。做完这一步再打开堆快照,点击列头 Retained Heap 排序,前 10 名才可信。
直方图只按类名统计实例数和总 shallow size,对排查泄漏几乎没用;而支配树强制体现“谁 hold 住谁”的唯一支配关系,每个节点的 retained size 是它自己 + 所有被它唯一支配的对象之和。关键操作:Open Query Browser → Java → Dominator Tree。重点关注三类节点:
com.example.service.CacheManager)排在前列ArrayList、ConcurrentHashMap)的 retained size 远大于其 shallow size
retained size 占堆 15% 以上右键某个高 retained size 对象 → Path to GC Roots → with all references。这里暴露的是它为什么活下来:如果路径里出现 java.lang.Thread.localMap,说明是 ThreadLocal 泄漏;如果出现 static 字段(如 MyClass.CACHE),说明静态缓存没做淘汰;如果出现 org.apache.catalina.loader.WebappClassLoader,基本锁定是 Web 应用未正确卸载导致 ClassLoader 泄漏。注意过滤掉 WeakReference 和 SoftReference 路径——它们不阻止 GC,不用优先处理。
真正难的不是找到大对象,而是确认它是否“该活”。同一个 ConcurrentHashMap 实例,如果是业务缓存且设置了 LRU 和过期策略,retained size 大是合理的;但如果它的 key 是不断 new 出来的 StringBuilder,value 是未关闭的 InputStream,那它就是泄漏源。得结合代码上下文判断,不能只盯数字。