当前位置: 首页 > 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.jsqmd.com/news/41622/

相关文章:

  • [嵌入式系统-150]:智能机器人(具身智能)内部的嵌入式系统以及各自的功能、硬件架构、操作系统、软件架构 - 实践
  • 2025年热门的滚筒烘干机厂家最新TOP实力排行
  • 2025年正规的水产封箱胶带厂家推荐及选择指南
  • 2025年靠谱的收费站网架用户口碑最好的厂家榜
  • 2025年合肥高压氧舱市场前景分析及顶级服务商排名
  • 2025年热门的高阻隔贴体膜厂家最新TOP排行榜
  • 作品实例
  • HTML5--------图片笔记
  • 深入解析:从 “坑“ 到 “通“:Spring AOP 通知执行顺序深度解密
  • 2025年热门的极简定制家具拉手厂家最新热销排行
  • the mission of English
  • 2025年桂圆品牌权威推荐榜单:优质选择与行业洞察
  • 2025年知名的胶印油墨厂家最新用户好评榜
  • 2025年比较好的无极绳气动煤矿道岔高评价厂家推荐榜
  • 十、HTML 符号
  • HTML4----------文字笔记
  • 2025年靠谱的淄博防爆潜水泵最新TOP厂家排名
  • 2025年口碑好的高温伴热带TOP实力厂家推荐榜
  • 2025年全新碳酸钙吨袋厂家最新热销排行
  • 2025年知名的污水格栅机厂家最新权威推荐排行榜
  • mysql查询数据的细节良好习惯
  • ehviewer白色版1.9.8.0使用教程
  • 2025年知名的边料粉碎机TOP实力厂家推荐榜
  • 2025年比较好的变流器高压直流继电器实力厂家TOP推荐榜
  • 002 vue3-admin项目的目录及文件说明之tsconfig.app.json
  • 2025年知名的保安公司推荐榜
  • 2025年知名的定制家具厂家推荐及选购指南
  • 11.11 联合查询 union /union all
  • WinForm 使用互斥锁防止应用重复打开
  • 11.10 外连接 自连接