一:背景

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<string> 数组,而这个 List<string> 又是一个临时对象,后续会被 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. 寻求化解之道

化解之道有很多:

  • 修改 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次

PerfView 洞察C#托管堆内存 "黑洞现象"的更多相关文章

  1. CLR via C# 读书笔记-21.托管堆和垃圾回收

    前言 近段时间工作需要用到了这块知识,遂加急补了一下基础,CLR中这一章节反复看了好多遍,得知一二,便记录下来,给自己做一个学习记录,也希望不对地方能够得到补充指点. 1,.托管代码和非托管代码的区别 ...

  2. C# 托管堆 遭破坏 问题溯源分析

    一:背景 1. 讲故事 年前遇到了好几例托管堆被损坏的案例,有些运气好一些,从被破坏的托管堆内存现场能观测出大概是什么问题,但更多的情况下是无法做出准确判断的,原因就在于生成的dump是第二现场,借用 ...

  3. PerfView专题 (第二篇):如何寻找 C# 中的 Heap堆内存泄漏

    一:背景 上一篇我们聊到了如何去找 热点函数,这一篇我们来看下当你的程序出现了 非托管内存泄漏 时如何去寻找可疑的代码源头,其实思路很简单,就是在 HeapAlloc 或者 VirtualAlloc ...

  4. "每日一道面试题".net托管堆是否会存在内存泄漏的情况

    首先说答案:会 所谓的内存泄漏,就是指内存空间上产生了不再被实际使用却又无非被分配的对象.严格意义上来说,在.net中经常会遇到内存泄漏的情况,因为托管堆内的对象不再被使用时,需要等待下一次GC才会被 ...

  5. C#内存管理之托管堆与非托管堆( reprint )

    在 .NET Framework 中,内存中的资源(即所有二进制信息的集合)分为“托管资源”和“非托管资源”.托管资源必须接受 .NET Framework 的 CLR (通用语言运行时)的管理(诸如 ...

  6. cir from c# 托管堆和垃圾回收

    1,托管堆基础 调用IL的newobj 为资源分配内存 初始化内存,设置其初始状态并使资源可用.类型的实列构造器负责设置初始化状态 访问类型的成员来使用资源 摧毁状态进行清理 释放内存//垃圾回收期负 ...

  7. 【C#进阶系列】21 托管堆和垃圾回收

    托管堆基础 一般创建一个对象就是通过调用IL指令newobj分配内存,然后初始化内存,也就是实例构造器时做这个事. 然后在使用完对象后,摧毁资源的状态以进行清理,然后由垃圾回收器来释放内存. 托管堆除 ...

  8. debug实战:Unmanaged High Memory非托管高内存

    最近又监控到一个高内存的问题,周五下班把系统打开,周末2天没关,周一来看已经涨到5.2G,这次与以往不同,不是.net的内存泄漏,而是非托管引起的. 1. 抓dump,确定高内存的类型 //dump有 ...

  9. C#中的托管堆和堆栈

    托管堆(Heap)和堆栈(Stack)是内存的逻辑划分.   栈 堆 连续性 连续 不连续 有序性 后进先出 无序 内存管理 操作系统自动释放 GC或人工 存放类型 值类型/引用 引用类型 注:内存格 ...

  10. C#堆栈和托管堆

    首先堆栈和堆(托管堆)都在进程的虚拟内存中.(在32位处理器上每个进程的虚拟内存为4GB) 堆栈stack 堆栈中存储值类型. 堆栈实际上是向下填充,即由高内存地址指向低内存地址填充. 堆栈的工作方式 ...

随机推荐

  1. flume组件以及通过命令监控大数据平台转态

    实验一.Flume 组件安装配置 1.下载和解压 Flume 可 以 从 官 网 下 载 Flume 组 件 安 装 包 , 下 载 地 址 如 下 URL 链 接 所 示 https://archi ...

  2. Django date

    date根据给定格式对一个日期变量进行格式化. 可用的格式字符串: 格式化字符 描述 示例输出a 'a.m.'或'p.m.' 'a.m.'A 'AM'或'PM' 'AM'b 月份,文字形式,3个字母, ...

  3. springboot~国际化Locale正确的姿势

    Java中的Locale.getDefault()获取的是操作系统的默认区域设置,如果需要获取客户端浏览器的区域设置,可以从HTTP头中获取"Accept-Language"的值来 ...

  4. SpringMVC 简单的开始

    SpringMVC简单的开始 利用Spring模板配置写一个web项目. 1.核心配置文件(模板代码) <?xml version="1.0" encoding=" ...

  5. Request header field userRole is not allowed by Access-Control-Allow-Headers in preflight response.

    今使用axios 请求数据过程中遇到同事设置的请求头,在我请求的接口中部不被允许,于是老是提示Request header field userRole is not allowed by Acces ...

  6. TypeError: Cannot read property 'getAttribute' of undefined

    今天使用echarts + vue 做 图标,运行时提示vue.runtime.esm.js?2b0e:619 [Vue warn]: Error in mounted hook: "Typ ...

  7. GTX.Zip:一款可以替代 gzip 的基因大数据压缩软件

    今天给大家推荐一款基因大数据压缩的大杀器:GTX.Zip. GTX.Zip 这款软件是由曾在 2016 年 GCTA 风云挑战赛中的那匹黑马--人和未来生物科技有限公司开发的,而当时他们也是打破了基因 ...

  8. 搭建私人GPT及域名配置

    前几天在掘金看到一个搭建私人ChatGPT的教程,看起来并不难. 我也有OpenAI的API Key,然后前阵子我看到我的账号余额还有很多,我的api key其实就一个机器人在用,没用多少. 还有,就 ...

  9. celery笔记四之在Django中使用celery

    本文首发于公众号:Hunter后端 原文链接:celery笔记四之在Django中使用celery 这一篇笔记介绍一下如何在 Django 系统中使用 celery. 如果是想纯粹使用 celery, ...

  10. 通过模仿学会Python爬虫(一):零基础上手

    好家伙,爬虫来了 爬虫,这玩意,不会怎么办, 诶,先抄一份作业回来 1.别人的爬虫  Python爬虫史上超详细讲解(零基础入门,老年人都看的懂)_ChenBinBini的博客-CSDN博客 # -* ...