DotMemory系列:1. 终结队列积压引发的内存暴涨分析
<h2 id="一背景">一:背景</h2><h3 id="1-讲故事">1. 讲故事</h3>
<p>说实话本来是不想写这个系列的,因为我潜意识里觉得这款工具就像美图秀秀一样,拉低专业人士的档次,但奈何在训练营里我需要用到 dottrace 这款工具,而我向官方申请再续了一年免费的Pack套件也给我通过了,所以我觉得要对得起他们,得要写点什么,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752410-827434363.png" alt="" loading="lazy"></p>
<p>这几天我也仔细看了下<code>DotMemory</code>的文档,发现还是有一些可圈可点的地方,毕竟<code>美图秀秀</code>也有<code>美图秀秀</code>的闪光点,在某些场景下完全可以用 DotMemory 作为WinDbg出场的第一套关卡,想来想去我决定还是写5篇托管内存故障来演示下DotMemory的使用,也确实它的可视化做的非常好,那这篇就先从 <code>终结队列积压</code> 导致的内存暴涨开始吧。</p>
<h2 id="二内存暴涨分析">二:内存暴涨分析</h2>
<h3 id="1-问题代码">1. 问题代码</h3>
<p>为了演示 <code>终结队列积压</code> 引发的内存暴涨,我故意让 <code>终结器线程</code> 处理的慢一些,这样就会存在不断的囤积情况,参考代码如下:</p>
<pre><code class="language-C#">
internal class Program
{
static void Main(string[] args)
{
for (int i = 1; i < 500000; i++)
{
NewPerson(i);
}
Console.WriteLine("50w 个对象插入完毕!");
Console.ReadLine();
}
static void NewPerson(int i)
{
var person = new Person()
{
ID = i + 1,
Name = string.Join(",", Enumerable.Range(0, 1000))
};
}
}
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
~Person()
{
Thread.Sleep(1000);
Console.WriteLine($"析构函数 {ID}: 执行完毕...");
}
}
</code></pre>
<h3 id="2-dotmemory-分析">2. DotMemory 分析</h3>
<p>这里我用的是 <code>DotMemory 2025.1</code> 版本,用 dotmemory 开启子进程的方式启动,大概三步走就行了,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752379-1264133615.png" alt="" loading="lazy"></p>
<p>这里一定要选择 <code>Sampled</code> 采样模式,如果选择 <code>Full</code> 模式那几乎是无法跑的,因为都是基于 ETW 的,所以和 perfview 的 <code>.NET SampleAlloc</code> 模式是一模一样的。</p>
<p>点击 Start 后,会有一个<code>内存用量</code>的动态图,在内存出现暴涨后,使用 <code>Get Snapshot</code> 采一个快照下来,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752384-1706557346.png" alt="" loading="lazy"></p>
<p>打开图中左下角的 <code>Snapshot #1</code> 快照,映入眼帘的就是 <code>Inspections</code> 视图,翻译过来用 <code>检测台</code> 比较合适,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752392-435382730.png" alt="" loading="lazy"></p>
<p>稍微熟悉 DotMemory 的朋友,看到快速通览之后肯定会发现问题所在,我就单独开一节来说吧!</p>
<h3 id="3-问题浮现">3. 问题浮现</h3>
<ol>
<li>Largest Size 环形图</li>
</ol>
<p>这个图是告诉大家某一类对象的浅层大小,即不包含他们的孩子节点,用 windbg 的话术就是直接取Person自身的 <code>Size=32byte</code>,很显然这 <code>32byte</code> 是不包含 <code>Person.<Name>k__BackingField</code> 的 <code>Size=7800byte</code> 的,输出如下:</p>
<pre><code class="language-C#">
0:015> !dumpobj /d 2e9693a2fa0
Name: Example_20_1_1.Person
MethodTable: 00007ffa0b5fa898
EEClass: 00007ffa0b6046a8
Tracked Type: false
Size: 32(0x20) bytes
Fields:
MT Field Offset Type VT Attr Value Name
00007ffa0b4b11884000001 10 System.Int321 instance 23906 <ID>k__BackingField
00007ffa0b52ec084000002 8 System.String0 instance 000002e9693a3000 <Name>k__BackingField
0:015> !DumpObj /d 000002e9693a3000
Name: System.String
MethodTable: 00007ffa0b52ec08
EEClass: 00007ffa0b50a5d8
Tracked Type: false
Size: 7800(0x1e78) bytes
String: 0,1,2,3,...
Fields:
MT Field Offset Type VT Attr Value Name
00007ffa0b4b1188400033b 8 System.Int321 instance 3889 _stringLength
00007ffa0b4bb538400033c c System.Char1 instance 30 _firstChar
00007ffa0b52ec08400033a c8 System.String0 static 000002e900000008 Empty
</code></pre>
<p>有了上面的思路之后,你应该就知道这个程序中吃的最多的就是String类型,总计 <code>3.63G</code>,对 String 产生重大怀疑之后,接下来就是看第二个环形图。</p>
<ol start="2">
<li>Largest Retained Size 环形图</li>
</ol>
<p>如果说刚才的图是不包含孩子节点的,那这张图就是切切实实的包含孩子节点,有些人可能要问,既然是包含关系,那包含的起点在哪里呢?熟悉 gc标记阶段的朋友应该知道,这个起点应该就是 root 根。</p>
<p>有了这个基础之后,你就应该能明白为什么 <code>Person</code> 类型的总量是排在第一位的,刚才的 windbg 输出已经告诉了我们,看样子 <code>Person.<Name>k__BackingField</code> 正是我们的问题所在。</p>
<ol start="3">
<li>String duplicates 问题</li>
</ol>
<p>在内功修炼训练营里跟大家分享过 <code>驻留池</code> 的底层原理,其实这个就是和 <code>驻留池</code> 有关,从卦中可以看到由 49.9w 的字符串理应都要进池子,结果都是以副本的形式存在于托管堆中,所以这里有了 <code>Wasted=3.63G</code> 一说,哈哈,到这里又看到了一处非常不合理的地方,也就说如果把这 49.9w 的string全部进池子,那么内存一下子就下去了,等一会我们来验证吧。</p>
<ol start="4">
<li>Finalizable Objects 问题</li>
</ol>
<p>这里有一个异常的信号,即 <code>红色感叹号</code>,说明这里可能存在一个大问题,从列表中可以看到 <code>Person=49.9w</code>,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752371-1132209120.png" alt="" loading="lazy"></p>
<p>这里的 <code>49.9w</code> 表示什么呢? 熟悉<code>clr终结队列</code>的朋友应该知道,这个 queued 其实就是 <code>freachable queue</code> 区域,即 终结器线程 提取对象的地方。</p>
<p>如果有些朋友还是搞不清楚,在我的训练营里有详细的画图说明,其中的 <code>深绿色区域</code> 就是所谓的提取区域,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752390-954768659.png" alt="" loading="lazy"></p>
<p>如果一定要在 dotmemory 上验证,那就双击呗,观察 <code>Similar Retention</code> 选项即可,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752377-370180629.png" alt="" loading="lazy"></p>
<p>言归正传,接下来的问题就来了,为什么 <code>终结器队列</code> 中有那么多的囤积?</p>
<h3 id="4-寻求问题之道">4. 寻求问题之道</h3>
<p>由于是采样模式,直接观察 <code>CallTree</code> 和 <code>Back Traces</code> 选项卡会不准,所以就直接观察 Person 的源代码,为什么 <code>析构函数</code> 这么不给力,很快就发现有不对的地方,这里居然有慢处理 <code>Thread.Sleep(1000)</code>,参考如下:</p>
<pre><code class="language-C#">
~Person()
{
Thread.Sleep(1000);
Console.WriteLine($"析构函数 {ID}: 执行完毕...");
}
</code></pre>
<p>这里稍微提醒一下,在真实场景中,一般会用 windbg 去观察此时的 <code>终结器线程</code> 的调用栈,但无奈 dotmemory 不具备观察线程的调用栈能力。</p>
<p>所以解决办法就比较简单了,将 <code>Thread.Sleep(1000);</code> 注释掉即可。</p>
<p>最后再说一种办法,也就是刚才说到了 wasted,如果全部送到驻留池,其实也是治标不治本的方法,但在这种场景下可以绝对的延迟OOM的时间,即用 <code>string.Intern</code> 给包起来,参考代码如下:</p>
<pre><code class="language-C#">
static void NewPerson(int i)
{
var person = new Person()
{
ID = i + 1,
Name = string.Intern(string.Join(",", Enumerable.Range(0, 1000)))
};
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752378-441348573.png" alt="" loading="lazy"></p>
<p>从卦中可以看到,其实送入了 50w 的超大 string,因为内存中只保有一份,所以再怎么大也大不起来,从检测台上也能看到那玩意在 <code>String duplicates</code> 列表中消失了,截图如下:</p>
<p><img src="https://img2024.cnblogs.com/blog/214741/202511/214741-20251114090752386-199151519.png" alt="" loading="lazy"></p>
<h2 id="三总结">三:总结</h2>
<p>DotMemory虽为美图秀秀,但秀秀也有秀秀的场景,在进一步深度分析之前,它是一款很好的快速通览利器。</p>
<img src="https://images.cnblogs.com/cnblogs_com/huangxincheng/345039/o_210929020104最新消息优惠促销公众号关注二维码.jpg" width="700" height="300" alt="图片名称" align="center"><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/19220465
頁:
[1]