PerfView专题 (第十六篇): 如何洞察C#托管堆内存的 "黑洞现象"
|
一:背景
1. 讲故事
首先声明的是这个 黑洞 是我定义的术语,它是用来表示 内存吞噬 的一种现象,何为 内存吞噬,我们来看一张图。
从上面的 卦象图 来看,GCHeap 的 Allocated=852M 和 Committed=16.6G,它们的差值就是 分配缓冲区=16G,缓冲区的好处就是用空间换时间,弊端就是会实实在在的侵占内存,挤压其他程序的生存空间。
二:黑洞现象
1. 为什么会有黑洞现象
万事皆有因果,今生的果是前世种的因,换句话说是程序曾经有大量及频繁的创建临时对象,让GC不自主的痉挛,小挛伤神,大挛伤身,所以GC为了避免大挛的发生,就大量的囤积本应该释放掉的内存,目的就是防止未来某个时刻再次有大内存分配的发生。
2. 重现今生的果
我相信因果关系大家都弄清楚了,但口说无凭,还得用代码证明一下不是?为了模拟GC痉挛,上一段测试代码。- public class Program
- {
- public static void Main(string[] args)
- {
- var builder = WebApplication.CreateBuilder(args);
- // Add services to the container.
- builder.Services.AddAuthorization();
- var app = builder.Build();
- // Configure the HTTP request pipeline.
- app.UseAuthorization();
- app.MapGet("/mytest", (HttpContext httpContext) =>
- {
- return MyTest();
- });
- app.MapGet("/gc", (HttpContext httpContext) =>
- {
- GC.Collect();
- return 1;
- });
- app.Run();
- }
- public static string MyTest()
- {
- List<string> list = new List<string>();
- for (int i = 0; i < 100000000; i++)
- {
- list.Add(i.ToString());
- }
- return "ok";
- }
- }
复制代码 代码非常简单,每请求一次 /mytest 都会分配一个 1亿 大小 List 数组,而这个 List 又是一个临时对象,后续会被 GC 回收,接下来我们多请求几次来调戏一下 GC,看他如何痉挛,截图如下:
从卦中看,我当前请求了 6 次,内存峰值达到了 12G,因为是临时对象,稍稍有一点回落,但此时已经撑成一个大胖子了,接下来我们用 WinDbg 附加一下,观察下 Allocated 和 Committed 阈值。- 0:033> !eeheap -gc
- ========================================
- Number of GC Heaps: 12
- ----------------------------------------
- ...
- Heap 11 (0000023513f26c10)
- generation 0 starts at 23351c3aab8
- generation 1 starts at 233484c38e0
- generation 2 starts at 233484c1000
- ephemeral segment allocation context: none
- Small object heap
- segment begin allocated committed allocated size committed size
- 0233484c0000 0233484c1000 02335c794ad0 023379ad2000 0x142d3ad0 (338508496) 0x31612000 (828448768)
- Large object heap starts at 234384c1000
- segment begin allocated committed allocated size committed size
- 0234384c0000 0234384c1000 0234384c1018 0234384e2000 0x18 (24) 0x22000 (139264)
- Pinned object heap starts at 234f84c1000
- segment begin allocated committed allocated size committed size
- 0234f84c0000 0234f84c1000 0234f84c1018 0234f84c2000 0x18 (24) 0x2000 (8192)
- ------------------------------
- GC Allocated Heap Size: Size: 0x14f241378 (5622731640) bytes.
- GC Committed Heap Size: Size: 0x2b125c000 (11561975808) bytes.
复制代码 从卦中看当前已经有 6G 的缓冲区了,为了让缓冲区更夸张,我们故意手工触发一次 GC 即请求 /gc,触发了GC之后,内存从 10G 回落到了 7G 就不再降了,截图如下:
从卦中看,这两个指标就更夸张了,GC 堆只有 1.1M 的对象,但预留了 7.1G 的内存。
这个GC表现不管在 道德 还是 伦理 上都说不通的。
3. 找到前世的因
要想找到前世的因,手段有很多,比如用 WinDbg 观察前世的托管堆,从残留的 Committed - Allocated上就能找到因,也可以使用 PerfView 实时观察,这里我们采用后者来洞察,使用默认的 Command 参数。- PerfView.exe "/DataFile:PerfViewData.etl" /BufferSizeMB:256 /StackCompression /CircularMB:500 /ClrEvents:GC,Binder,Security,AppDomainResourceManagement,Contention,Exception,Threading,JITSymbols,Type,GCHeapSurvivalAndMovement,GCHeapAndTypeNames,Stack,ThreadTransfer,Codesymbols,Compilation /NoGui /NoNGenRundown /Merge:True /Zip:True collect
复制代码 采集一段时间后停止采集,接下来双击 GC Heap Net Mem (Coarse Sampling) Stacks 选项再选择 WebApplication1 进程,通过 MaxMetric 指标看到曾经峰值达到了 10.9G,截图如下:
毫无疑问的说,内存峰值的时候必有妖怪,可以将峰值填入到 End 文本框中,然后双击内存占比最高的 System.String[],观察下它是谁分配的,截图如下:
从截图中可以清晰的看到,原来是 Program.MyTest() 造的孽,至此真相大白。
4. 寻求化解之道
化解之道有很多:
简而言之就是将 Server GC 改成 Workstation GC ,参考代码如下:- <Project Sdk="Microsoft.NET.Sdk">
- <PropertyGroup>
- <ServerGarbageCollection>false</ServerGarbageCollection>
- </PropertyGroup>
- </Project>
复制代码 默认情况一个 cpucore 有一个 heap,我们可以尽量的减少 heap.count 的个数,比如将 12 个改成 2 个。参考代码如下:- {
- "runtimeOptions": {
- "configProperties": {
- "System.GC.HeapCount": 2
- }
- }
- }
复制代码 导致今世的果 是因为在内存中短时的出现大对象,可以将大对象拆分成多批次的小对象处理,这样可以达到后浪推前浪的的内存复用,从源头上绕过这个问题。
三:总结
内存黑洞 虽不算 CLR 的一个bug,但绝对是 CLR 可优化的一个空间,分析这类问题是需要经验性的,分享出来供后来者少踩坑吧,毕竟在我的分析旅程中至少遇到了3次
来源:https://www.cnblogs.com/huangxincheng/archive/2023/07/24/17576542.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|
|
|
发表于 2023-7-24 12:37:20
举报
回复
分享
|
|
|
|