当前位置: 首页 > news >正文

DotMemory系列:3. 堆碎片化引发的内存暴涨分析

一:背景

1. 讲故事

前面两篇我们讲的都是通过挂引用根的方式导致的内存暴涨,在快速检测台上能够一眼就看出是什么类型的Type导致的,分析难度稍微较低,在真实的dump分析场景下,也会存在对象偏小而内存暴涨的情况,一般的新手会被这种场景搞懵逼,这篇就来分享这种奇葩的情况。

二:内存暴涨分析

1. 问题代码

为了方便演示,我们做这样的一个案例,现在的 .NET8 的SOH一个segment是 4M,所以我故意这么设计,分配3M的临时对象,然后再分配一个 50k 的Pinned对象,由于 Pinned 解封之前是GC不可移动对象,最终会导致 堆碎片化 现象,参考代码如下:

internal class Program{static void Main(string[] args){var harmony = new Harmony("com.example.gchandleallchook");harmony.PatchAll();ProcessData();Console.ReadLine();}static void ProcessData(){for (int i = 1; i <= 1000; i++){Allocate_Bytes(i);Allocate_Pinned(i);Console.WriteLine($"i={i} 次执行,3M byte[] 分配完毕,50k byte[] 分配完毕");}GC.Collect();Console.WriteLine("碎片化已形成,已强制执行GC,请观察托管堆!");}static void Allocate_Bytes(int i){//1k * 1024 * 3 = 3M (1个region)for (int j = 0; j < 1024 * 3; j++){var bytes = new byte[1024]; // 分配 3096 个 1k 的 byte[]}}static void Allocate_Pinned(int i){GCHandle.Alloc(new byte[1024 * 50], GCHandleType.Pinned); // 50k 的 pinned byte[]}}

代码有了之后,接下来就是用 dotMemory 把程序给跑起来,内存走势图如下所示。

从卦中可以看到,内存总计为 1.9G,其中 gen2 就独吃 1.8G,很显然这是托管内存泄露,接下来的操作就是采一个 snapshot,打开快速检测台,截图如下:

从检测台上看并没有看到哪一个类型的对象有占用过大的情况,这是不是让人匪夷所思呢?

2. 为什么对象占用不大

虽然对象占用不大,但内存确确实实被托管堆的gen2所吃,所以必须调转枪头直接观测检测台的尾部 Heap Fragmentation 区域,截图如下:

哈哈,一下子就发现了 gen2 区域的奇观,即使看不懂的话也会觉得奇奇怪怪的,接下来我就简单分析下这里面的几个指标吧。

  1. heap: 表示当前有 810 个 segment 内存段
  2. total: 表示当前 gen2 吃了 1.77G 内存。
  3. used(pinned):表示 1.77G 内存中,pinned 对象占了 48.8M 内存。
  4. used(unpinned): 表示 1.77G 内存中未固定对象吃了 46.8k 内存。
  5. free: 表示当前空闲块吃了 1.73G。

上面几个指标合起来就是说 gen2 用 1.77G 内存只装近 50M 的对象,这种奇葩现象就是所谓的 堆碎片化

接下来就是要寻找这些 pinned 对象,他们到底是什么,为什么让 GC 痛苦不堪,可以选择 Generations 选项卡,双击其中任一个segment,截图如下:

打开面板之后发现都是 Byte[] 数组,通过 Similar retention 选项卡发现都是 Pinning handle ,即通过 GCHandleType.Pinned 固定的,截图如下

接下来的问题是这些 Byte[] 数组到底是被谁固定的?为什么不解开呢?

2. byte[] 是谁创建的

如果把这个问题搞定了,那所有的真相就会大白,那怎么做呢?一般来说有两种做法,第一种就是 full 采集模式,然后观察 byte[] 的调用栈即可,还有一种方式使用 harmony 注入的方式记录调用栈。这里都给大家介绍一下吧。

  1. full 采集模式

首先要说的是 full 采集模式在真实环境下很难实行,因为它对程序的性能伤害太大了,这个在官方文档中也有所说明,截图如下:

最后选择 Start 按钮开始采集,按照前面所述的方式找到 byte[] 数组再选择 Back Traces 选项卡,可以清楚的看到是 Allocate_Pinned() 方法创建的。

刚才是通过 type 为依据寻找的调用栈,也可以找到具体的 byte[] 实例观察其 Create Stack Trace 选项,同样也能看到,截图如下:

刚才也说了,这种方式虽然可行,但不是第一手段,更合适做万不得已的备份方案,万一程序能受得了这么重的暴击呢?

  1. harmony 注入

第二种方式就是脱离 dotmemory,采用一种 IL 注入的方式,原理非常简单,就是在 SDK 的 GCHandle.Alloc 内部增加日志,参考代码如下:


public static GCHandle Alloc(object? value, GCHandleType type)
{// prefix: todo...return new GCHandle(value, type);// postfix:todo...
}

在 postfix 中我们记录下调用 Alloc 方法的调用栈,这样是不是就真相大白了,完整的参考代码如下:

internal class Program{static void Main(string[] args){var harmony = new Harmony("com.example.gchandleallchook");harmony.PatchAll();ProcessData();Console.ReadLine();}static void ProcessData(){for (int i = 1; i <= 1000; i++){Allocate_Bytes(i);Allocate_Pinned(i);Console.WriteLine($"i={i} 次执行,3M byte[] 分配完毕,50k byte[] 分配完毕");}GC.Collect();Console.WriteLine("碎片化已形成,已强制执行GC,请观察托管堆!");}static void Allocate_Bytes(int i){//1k * 1024 * 3 = 3M (1个region)for (int j = 0; j < 1024 * 3; j++){var bytes = new byte[1024]; // 分配 3096 个 1k 的 byte[]}}static void Allocate_Pinned(int i){GCHandle.Alloc(new byte[1024 * 50], GCHandleType.Pinned); // 50k 的 pinned byte[]}}[HarmonyPatch(typeof(GCHandle), "Alloc", new Type[] { typeof(object), typeof(GCHandleType) })]public class GCHandleAllocHook{public static void Postfix(GCHandle __result, GCHandleType type){if (type == GCHandleType.Pinned){Console.WriteLine($"  - 句柄指针: 0x{GCHandle.ToIntPtr(__result).ToInt64():X}");Console.WriteLine($"  - 句柄类型: {type}");Console.WriteLine(Environment.StackTrace);}}}

最后运行程序,观察日志输出即可,截图如下:

从卦中日志看是不是轻松的就找到了 Allocate_Pinned() 方法,在真实场景中还是建议大家写到 Nlog 这样的日志框架中。

三:总结

DotMemory 在可视化方面做的还是蛮强大的,感觉特别适合作为 技术支持工程师 的首选工具,希望本篇能给你带来一些帮助。

图片名称
http://www.zskr.cn/news/50969.html

相关文章:

  • 2025年热门的滚筒烘干机厂家最新TOP实力排行
  • 2025年正规的水产封箱胶带厂家推荐及选择指南
  • HTML5--------图片笔记
  • the mission of English
  • 2025年桂圆品牌权威推荐榜单:优质选择与行业洞察
  • HTML4----------文字笔记
  • 2025年口碑好的高温伴热带TOP实力厂家推荐榜
  • mysql查询数据的细节良好习惯
  • ehviewer白色版1.9.8.0使用教程
  • 2025年比较好的变流器高压直流继电器实力厂家TOP推荐榜
  • 11.11 联合查询 union /union all
  • 11.10 外连接 自连接
  • 2025年质量好的铸铜加热器厂家推荐及选择参考
  • 厂房通风天窗公司,一字型通风天窗厂家推荐,通风天窗厂家销售厂家,三角型通风天窗公司,消防排烟通风天窗工厂有哪些-鋆之昊环保科技
  • 2025年评价高的VR工厂全景视频拍摄制作技术领先品牌榜
  • 2025年靠谱的打浆机高箱款用户好评厂家排行
  • 2025年质量好的短视频推广优质服务排行榜
  • 2025年热门的液压异型铰链用户好评厂家排行
  • 11.7 多表查询 内连接
  • 2025年11月亚克力板材厂家推荐榜:川企领衔五强对比评测
  • ai学习机哪个品牌好?2025年十大学习机品牌
  • 详细介绍:【设计模式】Java规则树重构复杂业务逻辑
  • 2025年11月GEO公司TOP5推荐:全域智能营销解决方案深度解析
  • 实用指南:《让 Python 飞入浏览器:PyScript 与 WebAssembly 的魔法联动》
  • 11.4 约束
  • 2025年11月精华油产品TOP5推荐:抗衰功效与肤感平衡深度对比
  • 完整教程:MATLAB基于混合算法改进灰色模型的装备故障预测
  • 未来之窗昭和仙君(二十五)诊所看诊框架——东方仙盟筑基期
  • qt6 wayland widget设置位置不起作用
  • 飞机汉化