游乐游手机版
首页/前端开发/文章详情

如何分析堆快照中的“保留大小”快速定位最耗费内存的代码对象

时间:2026-04-24 20:46
如何分析堆快照中的“保留大小”快速定位最耗费内存的代码对象 什么是保留大小(Retained Size)? 说到内存分析,很多人第一反应是看对象自己有多大。但这里有个更关键的概念:保留大小。它衡量的不是对象自身占了多少字节,而是回答一个更实际的问题——如果把这个对象从内存里“连根拔起”,能顺带释放出

如何分析堆快照中的“保留大小”快速定位最耗费内存的代码对象

如何分析堆快照中的“保留大小”快速定位最耗费内存的代码对象

什么是保留大小(Retained Size)?

说到内存分析,很多人第一反应是看对象自己有多大。但这里有个更关键的概念:保留大小。它衡量的不是对象自身占了多少字节,而是回答一个更实际的问题——如果把这个对象从内存里“连根拔起”,能顺带释放出多少空间?

举个例子,一个 HashMap 实例的 retained size 如果特别大,那事情就很有意思了。这意味着,不仅仅是这个 HashMap 对象本身,它里面装的所有键值对、支撑这些键值对的内部数组,甚至这些值对象下游引用的整个对象网络(比如一堆 StringArrayList),全都因为它的存在而“活”着,GC 动不了它们。这才是真正卡住内存脖子的那个“关键先生”。

相比之下,浅层大小(shallow size)只算对象头和字段指针,太表面,容易误判;深层大小(deep size)倒是把引用链上的都算进来了,但它有个问题:会把那些被其他路径强引用的对象也算进去,干扰我们定位真正的“罪魁祸首”。

MAT 中按 Retained Size 排序时必须关掉“Keep unreachable objects”

打开 MAT 分析堆快照,直接按 Retained Heap 排序,你以为排在前面的就是“元凶”?先别急,这里有个常见的坑。

默认情况下,MAT 会勾选“Keep unreachable objects”选项。这个选项会让分析结果里混入大量已经不可达、本该被 GC 回收但还没来得及清理的对象。比如,某个大对象刚刚被程序置为 null,但 GC 还没跑,它就会出现在列表里。这类对象的 retained size 往往是 0 或者极小,但它们会挤占列表顶部的位置,把真正有问题的活对象给“淹没”了。

所以,正确的操作姿势是:先进入 Preferences → Memory Analyzer,找到 Keep unreachable objects 并取消勾选。做完这一步,再重新打开堆快照,点击 Retained Heap 列头进行排序。这时候,排在前 10 名的对象,才值得我们花时间深究。

看“支配树(Dominator Tree)”比看“直方图(Histogram)”更准

排查内存问题,很多人习惯先看直方图。但说实话,直方图主要按类名统计实例数和总 shallow size,对于定位泄漏点,帮助其实很有限。

真正的高手,会直接打开“支配树”。为什么?因为它强制体现了对象间“谁真正 hold 住谁”的唯一支配关系。在支配树里,每个节点的 retained size 就是它自己,加上所有被它唯一支配(即除了通过它,没有其他路径可达)的对象的总和。这直接对应了“删除它能释放多少内存”的核心问题。

操作路径很简单:Open Query Browser → Ja va → Dominator Tree。打开后,重点关注以下几类节点:

  • 排在前列的,是非 JDK 的内部业务类,比如 com.example.service.CacheManager
  • 集合类(像 ArrayListConcurrentHashMap)的 retained size 如果远大于其自身的 shallow size,说明它肚子里“装”了很多东西。
  • 有时候,泄漏不是单个实例造成的,而是多个同类的实例“分头作案”。如果某个类的多个实例分散在不同路径,但它们的 retained size 合计超过了堆的 15%,那也值得高度警惕。

点开对象后重点看 “Path to GC Roots” 里的“with all references”

找到 retained size 大的对象只是第一步。接下来要问:它为什么能活着?是谁在“保”它?

这时候,右键点击这个可疑对象,选择 Path to GC Roots → with all references。这条路径会清晰地告诉你,是什么引用链让它依然坚挺地留在堆里。

路径里透露的信息往往是破案的关键:

  • 如果出现了 ja va.lang.Thread.localMap,那基本可以断定是 ThreadLocal 使用不当导致的内存泄漏。
  • 如果路径指向了一个 static 字段(比如 MyClass.CACHE),那问题很可能出在静态缓存没有设置合理的淘汰机制上。
  • 如果看到了 org.apache.catalina.loader.WebappClassLoader 这类类加载器,那几乎可以锁定是 Web 应用热部署或卸载时,ClassLoader 泄漏的经典场景。

当然,也要注意过滤噪音。查看时可以排除 WeakReferenceSoftReference 这类引用路径——它们本身不阻止 GC,通常不是优先处理的目标。

话说回来,最难的部分往往不是找到那个“大块头”,而是判断它该不该这么大。同一个 ConcurrentHashMap 实例,如果它是业务核心缓存,并且设计了完善的 LRU 和过期策略,那么 retained size 大是合理的,是功能需要。但如果发现它的 key 是不断新创建的 StringBuilder,value 里还挂着一堆未关闭的 InputStream,那它无疑就是内存泄漏的源头。所以,最终一定要结合代码的业务上下文来做判断,不能光盯着数字下结论。

来源:https://www.php.cn/faq/2339684.html
上一篇HTML怎么做canvas时钟_HTML canvas时钟表盘绘制教程【方法】 下一篇网页如何使用自定义数据属性?Data-*属性存储私有变量
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
如何在JavaScript中实现基于旋转视野的FOV射线绘制详解
前端开发 · 2026-07-01

如何在JavaScript中实现基于旋转视野的FOV射线绘制详解

如果用一句话概括核心,那就是:在 RayCasting 游戏开发中,绘制动态视野边界线(FOV)最可靠的方式是在逻辑层通过数学公式将坐标“算”出来,而不是依赖 Canvas 绘图上下文的旋转操作。 在实现类似 Doom 风格的 RayCasting 游戏时,动态视野(Field of View, F

TypeScript后端数据正确映射为前端接口类型的方法
前端开发 · 2026-07-01

TypeScript后端数据正确映射为前端接口类型的方法

在后端数据与前端类型之间来回转换,几乎是每位 TypeScript 开发者都无法回避的常态。后端返回的 car_brand、reg_number,和前端接口中定义的 brand、govtNumber,命名风格常常对不上号。此时,如果为了省事直接用 as 类型断言“强行”指认类型,那就踩进了常见的陷阱

动态HTML表格按层级条件合并单元格的JavaScript实现
前端开发 · 2026-07-01

动态HTML表格按层级条件合并单元格的JavaScript实现

本文详细讲解一种递归式 JavaScript 合并单元格方法,用于按列优先级(如前3列)智能合并表格行:仅当前一列已合并的前提下,才允许后续列合并相同值,从而精准实现多级分组与层级表格合并效果。 在动态生成的 HTML 表格中,按业务逻辑合并重复行是常见需求。然而,简单地对单列分别遍历合并——例如先

Next.js 13+重定向后滚动失效解决方案
前端开发 · 2026-07-01

Next.js 13+重定向后滚动失效解决方案

在 Next js App Router 的日常开发中,有一个令人颇为困扰的异常现象——当服务端执行 `redirect()` 跳转后,目标页面竟然无法正常滚动。没错,页面已经渲染完成,内容也完整显示,但垂直滚动条仿佛凭空消失。这个问题在 Next js 13 5 4 版本中尤为突出。 先给出结论:

WebGL图像加载延迟的纹理初始化时立即显示方法
前端开发 · 2026-07-01

WebGL图像加载延迟的纹理初始化时立即显示方法

本文详细介绍如何利用 Promise 与 async await 重构 WebGL 纹理加载流程,彻底解决首次渲染显示蓝色占位色、需要手动交互才能刷新的问题,实现文件导入后四张纹理平面即时正确渲染。 实际上,这个坑在 WebGL 开发中相当常见——纹理异步加载的小陷阱,说起来不大,但第一次遇到确实令