PerfView 洞察C#托管堆内存 "黑洞现象"

这篇具有很好参考价值的文章主要介绍了PerfView 洞察C#托管堆内存 "黑洞现象"。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一:背景

1. 讲故事

首先声明的是这个 黑洞 是我定义的术语,它是用来表示 内存吞噬 的一种现象,何为 内存吞噬,我们来看一张图。

PerfView 洞察C#托管堆内存 "黑洞现象"

从上面的 卦象图 来看,GCHeap 的 Allocated=852MCommitted=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<string> 数组,而这个 List<string> 又是一个临时对象,后续会被 GC 回收,接下来我们多请求几次来调戏一下 GC,看他如何痉挛,截图如下:

PerfView 洞察C#托管堆内存 "黑洞现象"

从卦中看,我当前请求了 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 就不再降了,截图如下:

PerfView 洞察C#托管堆内存 "黑洞现象"

从卦中看,这两个指标就更夸张了,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,截图如下:

PerfView 洞察C#托管堆内存 "黑洞现象"

毫无疑问的说,内存峰值的时候必有妖怪,可以将峰值填入到 End 文本框中,然后双击内存占比最高的 System.String[],观察下它是谁分配的,截图如下:

PerfView 洞察C#托管堆内存 "黑洞现象"

从截图中可以清晰的看到,原来是 Program.MyTest() 造的孽,至此真相大白。

4. 寻求化解之道

化解之道有很多:

  • 修改 GC 模式

简而言之就是将 Server GC 改成 Workstation GC ,参考代码如下:


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <ServerGarbageCollection>false</ServerGarbageCollection>
  </PropertyGroup>

</Project>

  • 修改 Heap 个数

默认情况一个 cpucore 有一个 heap,我们可以尽量的减少 heap.count 的个数,比如将 12 个改成 2 个。参考代码如下:


{
   "runtimeOptions": {
      "configProperties": {
         "System.GC.HeapCount": 2
      }
   }
}

  • 大事化小

导致今世的 是因为在内存中短时的出现大对象,可以将大对象拆分成多批次的小对象处理,这样可以达到后浪推前浪的的内存复用,从源头上绕过这个问题。

三:总结

内存黑洞 虽不算 CLR 的一个bug,但绝对是 CLR 可优化的一个空间,分析这类问题是需要经验性的,分享出来供后来者少踩坑吧,毕竟在我的分析旅程中至少遇到了3次 😂😂😂文章来源地址https://www.toymoban.com/news/detail-599198.html

PerfView 洞察C#托管堆内存 "黑洞现象"

到了这里,关于PerfView 洞察C#托管堆内存 "黑洞现象"的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • PerfView专题 (第十四篇): 洞察那些 C# 代码中的短命线程

    这篇文章源自于分析一些疑难dump的思考而产生的灵感,在dump分析中经常要寻找的一个答案就是如何找到死亡线程的生前都做了一些什么?参考如下输出: 前面的 XXXX 代表线程已死亡,那谁能告诉我 ID=22 的线程生前执行了什么代码呢?其实去年我写了一篇如何用 WinDbg 去寻找

    2024年02月17日
    浏览(47)
  • 如何洞察 .NET程序 非托管句柄泄露

    很多朋友可能会有疑问,C# 是一门托管语言,怎么可能会有非托管句柄泄露呢? 其实一旦 C# 程序与 C++ 语言交互之后,往往就会被后者拖入非托管泥潭,让我们这些调试者被迫探究 非托管领域问题 。 为了方便讲述,我们上一个 Event 泄露的案例,使用 C# 调用 C++ ,然后让

    2024年02月12日
    浏览(43)
  • 盘点C#中感叹号"!"的作用

    在C#编程语言中,感叹号\\\"!\\\"是一个常见的符号,具有多种用途和作用。小编在本文将带大家探讨感叹号在C#中的不同用法,帮助程序员更好地理解和运用这个小而重要的符号。 1、逻辑非: 感叹号用作逻辑非运算符,可以对布尔值进行取反操作。通过在布尔表达式前面添加感叹

    2024年02月11日
    浏览(29)
  • C#实现Windows中"用xxx打开"

    在Windows中, 将文件用鼠标拖动到一个程序上面, 会有一句小提示: 用 xxx 打开(如下图)。   它本质上就是运行程序时, 传递了一个文件路径的命令行参数。 相当于这样的命令:   其中\\\"program.exe\\\"就是打开此文件用的程序, \\\"file_path\\\"参数就是拖过去的文件的绝对路径。 因此我们可以

    2024年02月16日
    浏览(37)
  • 如何洞察 C# 程序的 GDI 句柄泄露

    前段时间有位朋友找到我,说他的程序界面操作起来很慢并且卡顿等一些不正常现象,从任务管理器看了下 GDI句柄 已经到 1w 了,一时也找不出什么代码中哪里有问题,让我帮忙看下,其实这种问题看内存dump作用不是很大,主要是写脚本很麻烦,这一篇我们就来简单聊聊如何

    2024年02月08日
    浏览(71)
  • 【C/C++】栈内存布局,堆栈内存被破坏的现象及可能原因

    栈内存是一种由编译器自动分配和释放的内存区域,用于存储函数调用时的局部变量、函数参数、返回地址等信息。栈内存的分配和释放是由程序自动完成的,通常不需要手动管理。栈内存的布局通常是从高地址向低地址生长,每个函数调用时都会在栈上分配一段空间,称为

    2024年02月11日
    浏览(26)
  • C#探索之路(4):浅析C#中的托管、非托管堆栈与垃圾回收

    1、 托管代码: 1、使用 .NET 时,我们经常会遇到“托管代码”这个术语。 2、简而言之,托管代码就是执行过程交由运行时管理的代码。 2、CLR阶段: 1、在托管服务下,相关的运行时称为公共语言运行时 (CLR)。 2、不管使用的是哪种实现(例如 Mono、.NET Framework 或 .NET Core/.NET

    2024年02月05日
    浏览(45)
  • flink内存管理(三):MemorySegment内存使用场景:托管内存与网络内存

    在Flink内存模型中我们已经知道,Flink会将内存按照使用方式、内存类型分为不同的内存区域,底层会借助MemorySegment对内存块进行管理和访问,MemorySegment的使用场景有很多,本文我们主要看下 ManagedMemory和NetworkBuffer是如何申请和使用MemorySegment内存块的。 Task使用的物理计算资

    2024年01月22日
    浏览(34)
  • 聊一聊 Valgrind 监视非托管内存泄露和崩溃

    只要是程序总会出现各种莫名其妙的问题,比如:非托管内存泄露,程序崩溃,在 Windows 平台上一般用微软自家的官方工具 App Verifier 就可以洞察,那问题出在 Linux 上怎么办呢?由于 Linux 崇尚自由,需要在各种牛鬼蛇神写的非官方开源软件中寻找一个比较靠谱的,比如本篇所

    2024年02月02日
    浏览(70)
  • C#(.Net) 将非托管dll嵌入exe中

    前往我的主页以获得更好的阅读体验 C#(.Net) 将非托管dll嵌入exe中 - DearXuan的主页 https://blog.dearxuan.com/2021/12/26/C-Net-%E5%B0%86%E9%9D%9E%E6%89%98%E7%AE%A1dll%E5%B5%8C%E5%85%A5exe%E4%B8%AD/ 托管dll实际上是指C#编写的dll,可以直接右键“引用”导入 而大部分情况下,我们需要引用C++写的dll,如果你

    2024年02月08日
    浏览(25)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包