一:背景

1. 讲故事

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

从上面的 卦象图 来看,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,看他如何痉挛,截图如下:

从卦中看,我当前请求了 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. javasec(五)URLDNS反序列化分析

    这篇文章介绍 URLDNS 就是ysoserial中⼀个利⽤链的名字,但准确来说,这个其实不能称作"利⽤链".因为其参数不是⼀个可以"利⽤"的命令,⽽仅为⼀个U ...

  2. CVE-2016-3088漏洞复现

    1.背景介绍. ActiveMQ的web控制台分三个应用,admin.api和fileserver,其中admin是管理员页面,api是接口,fileserver是储存文件的接口:admin和api都 ...

  3. 2022-12-31:以下go语言代码输出什么?A:1 1;B:-1 1;C:-1 -1;D:编译错误。 package main import “fmt“ func main() { a

    2022-12-31:以下go语言代码输出什么?A:1 1:B:-1 1:C:-1 -1:D:编译错误. package main import "fmt" func main() ...

  4. 2021-03-11:go中,协程内部再启用协程,它们是没关系,对吧?外部协程奔溃,内部协程还会执行吗?外部协程执行结束的时候,如何让内部协程也停止运行?golang原生提供的包里,让内部协程停止运行,如何实现?

    2021-03-11:go中,协程内部再启用协程,它们是没关系,对吧?外部协程奔溃,内部协程还会执行吗?外部协程执行结束的时候,如何让内部协程也停止运行?golang原生提供的包里,让内部协程停止运行 ...

  5. 天下苦 Spring 久矣,Solon v2.2.20 发布

    Solon 是什么框架? 一个,Java 新的生态型应用开发框架.它从零开始构建,有自己的标准规范与开放生态.与其他框架相比,它解决了两个重要的痛点:启动慢,费资源. 解决痛点? 由于Solon Be ...

  6. nodejs和npm升级版本

    由于服务器环境的不同可能需要根据实际情况升降对应的nodejs 及npm 版本,最简单的例子就是 npx 只适用于 npm 5+ 看想用npx 那不升级咋办呢,还有如error eslint@7.16 ...

  7. golang 包管理

  8. chess草稿(附代码!)

    2022/8/12日过了,代码如下:(已删除调试语句,保留注释,为了使代码更容易看懂并没有卡常.卡完常的代码不是给人看的) 点击查看代码 /* 倒序操作+合并连通块+维护集合,支持合并.区间查询+线段 ...

  9. 信息收集_网络扫描_nmap

    信息收集_网络扫描nmap 目标说明 -iL <inputname> (从列表或文件输入) -iR <hostnum> (随机选择生成目标数量) --exclude <h ...

  10. 【HarmonyOS】关于 Caused by java.lang.IllegalStateException The specified...

    [问题描述] 线上收到大量手机的崩溃异常,以华为手机为主,崩溃如下 1.Caused by: java.lang.IllegalStateException: The specified messag ...