一、为什么需要dotMemory?

当你发现在重复执行某个动作后,内存只增不减,可用内存越来越少;或者因为内 存问题导致卡顿;或者代码产生了过多,过大的临时对象,导致垃圾回收频繁触发,拖 慢程序运行速度时,dotMemory可以帮你看清内存中到底发生了什么,并帮你找到“罪 魁祸首”。

dotMemory官方文档: https://www.jetbrains.com/zh-cn/help/dotmemory/2025.3/dotMemory_Introduction.html

二、dotMemory内存问题排查

1.开始分析会话

在Profiling菜单中点击上方新建按钮“+”,选择Standalone(桌面或控制台程序),选择要分析的exe路径,保存,接下来进行配置分析会话选项,Sampled满足大多数场景,图中1,进程筛选根据需要可以自定义,默认就选整个进程树,图中2,控制分析方式选手动,图中3。点击Start按钮将启动程序开始分析;也可选择左下角运行中的程序启动分析。

2.获取快照

选择合适的时机点击Get Snapshot获取想要的快照,快照是应用程序托管堆的即时映像,根据当前怀疑某个动作导致了内存问题在其前后分别拍照,用于比较。单个快照基础信息包含total:整个应用程序消耗的内存大小,.net total:托管堆中的内存总量,包含空闲内存,.net used:应用程序使用了的托管堆中的内存,其余为非托管内存

当对象刚被创建时,它们会被放置在第 0 代(Gen 0)。 当第 0 代已满时,GC 会执行一次垃圾回收。 在回收过程中,GC 会从堆中移除所有不可达的对象。 所有可达的对象会被提升到第 1 代(Gen 1)。 当第 1 代已满时,会执行第 1 代的垃圾回收。 所有在回收中存活的对象会被提升到第 2 代(Gen 2)。 第 0 代的回收也会在此时进行。 当第 2 代已满时,GC 会执行完整垃圾回收。 首先执行第 2 代的回收,之后依次执行第 1 代和第 0 代的回收。 如果仍然没有足够的内存进行新的分配,GC 会引发 OutOfMemory 异常。

3.大对象堆

 在.NET中,大对象堆(Large Object Heap, LOH)是一块特殊的内存区域,专门用于存放大型对象。下表概括了LOH的核心特性

特性

描述

阈值

对象大小>85KB

回收世代

仅在 第2代垃圾回收(Full GC)​ 时被回收

内存整理

默认不进行压缩整理(但可通过API或高版本.NET优化)

典型对象

大型数组(如 byte[], int[],double[],float[])、长字符串等

LOH的运作细节及性能

  1. 分配成本较高:CLR为确保新对象内存的清洁,会在分配时进行清零操作。对于大对象,这个过程需要消耗较多的CPU周期
  2. 容易产生内存碎片:这是LOH最主要的问题。由于默认不压缩,当多个大对象被分配和释放后,堆上会留下许多不连续的“空闲空间”。即使总空闲内存很大,也可能因为没有足够大的连续空间来分配一个新对象,导致触发不必要的完整垃圾回收,甚至引发OutOfMemoryException
  3. 对引用类型数组的特别关注:如果LOH上存放的是包含大量引用类型元素的数组(例如object[]或自定义类数组),GC在标记存活对象时需要遍历这些引用,会增加GC的负担。如果元素是值类型(如int, double),则GC无需处理其内部。

优化策略

  1. 首选对象复用:避免频繁地创建和销毁大对象。使用对象池(Object Pooling)​ ,特别是 ArrayPool<T>类,它可以池化数组供临时使用,用完后归还,极大地减少LOH的压力和碎片化
  2. 手动压缩LOH:在.NET Framework 4.5.1及更高版本(包括.NET Core/.NET 5+)中,可以通过设置 GCSettings.LargeObjectHeapCompactionMode属性,让下一次完整的阻塞式GC对LOH进行压缩     GCSettings.LargeObjectHeapCompactionMode =                                        GCLargeObjectHeapCompactionMode.CompactOnce;     GC.Collect();
  3. 避免固定大对象:使用fixed语句或GCHandle固定大对象会阻止GC移动它们,这可能加剧碎片化问题。应尽量避免长时间固定大对象

4.观察内存快照

点击快照名称打开快照,可以点击+按钮对相同数据执行多个独立的分析

观察内存快照--Inspections(检查):显示主要的快照热点

  1. Largest Size(最大大小) :该图表显示了特定类型对象占用的内存量。
  2. Largest Retained Size(最大的保留大小) :该图表显示了关键对象,即在内存中保留应用程序中所有其他对象的对象。
  3. String duplicates, Sparse arrays,Event handlers leak,等:dotMemory 会自动检查快照中最常见的内存问题类型。
  4. 堆碎片 :显示了托管堆段的碎片情况: 第 1 代、 2 和 大对象堆。

在Types视图中查看哪些对象占用了大量内存

当前视图按消耗内存量排序,图中大部分内存被Single[]占用,双击打开Single[],或右键点击选择打开此对象集,选择Back Traces(分析会话如果选择Sampled可能不显示调用路径), 可以看到这些Single[]数组来源WaveDataPreview.GenerateAdvancedWaveTest1()方法

5.比较快照

在可疑操作前后分别获取快照用于比较,点击下方Compare进入比较界面,在该界面可以看到两次快照中当前存活对象,新对象,死对象,通过内存变化量进行排序查看什么类型在涨

选中增长最多的类型,点击存活对象,死对象,新对象下方的数字,查看具体的对象实例,查看对象的详细信息

6.分析内存分配

在时间轴上选择区间,打开“内存分配”视图,分析哪些类型被造得最多,谁造的,调用链是什么,哪个模块干的?图中1、2、3都可打开内存分配视图

7.内存泄漏排查举例

在给lightingchart控件添加曲线时将该曲线也添加到另一集合中,重复操作,内存持续增长,调用清理,内存正常


总结

通过上面的方法可以快速分析C#程序内存占用情况,并定位到对应的代码中。利于我们快速优化内存占用问题。

工具下载地址:【免费】dotMemory工具资源-CSDN下载

更多推荐