一:背景

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. 20130625-关于mac配置android cocos2dx

    1.下载cocos2dx  ndk  eclipse http://developer.android.com/tools/sdk/ndk/index.html 2.cocos2dx文件中找到crea ...

  2. javasec(三)类加载机制

    这篇文章介绍java的类加载机制. Java是一个依赖于JVM(Java虚拟机)实现的跨平台的开发语言.Java程序在运行前需要先编译成class文件,Java类初始化的时候会调用java.lang. ...

  3. 明解STM32—GPIO应用设计篇之API函数及配置使用技巧

    一.前言 本篇开始对STM32的GPIO在实际开发设计中的使用配置和技巧进行探讨,可以先去回顾下之前介绍的GPIO的相关理论基础知识包括基本结构,工作模式和寄存器原理. 了解过STM32的GPIO相关 ...

  4. 笔记七:进程间的通信(IPC通信之信号灯)

    信号量 定义信号变量:  sem_t sem1 sem2 初始化信号量 P操作           V操作    功能 信号量(POSOX) 信号量灯(IPC) 定义信号变量 sem_t  sem1 ...

  5. Java的对象包装器 & 自动装箱

    有时,需要将 int 这样的基本类型转换为对象.所有的基本类型都有一个与之对应的类.例如,Integer 类对应基本类型 int.通常,这些类被称为包装器(wrapper).这些对象包装器类拥有很明显 ...

  6. 超实用的Go语言基础教程,让你快速上手刷题!!

    背景 工欲善其事,必先利其器.掌握Go的基础语法还不够,还需要勤加练习,修习"外功",才能达到出奇制胜的效果. 在大致了解Go语言的基本语法后,我就迫不得已地想使用这门语言.可是我 ...

  7. 2022-02-15:扫地机器人。 房间(用格栅表示)中有一个扫地机器人。 格栅中的每一个格子有空和障碍物两种可能。 扫地机器人提供4个API,可以向前进,向左转或者向右转。每次转弯90度。 当扫地机

    2022-02-15:扫地机器人. 房间(用格栅表示)中有一个扫地机器人. 格栅中的每一个格子有空和障碍物两种可能. 扫地机器人提供4个API,可以向前进,向左转或者向右转.每次转弯90度. 当扫地机 ...

  8. Django笔记四十之运行Django环境的python脚本

    本文首发于公众号:Hunter后端 原文链接:Django笔记四十之运行Django环境的python脚本 这一篇笔记介绍如何在 Django 中运行脚本. 假设说我们要实现一个功能,需要获取 blo ...

  9. 原来.NET写的Linux桌面这么好看?

    如何使用Blazor在Linux平台下运行Desktop程序 本文将讲解如何使用Blazor运行跨平台应用,应用到的技术有以下几点 Blazor Masa Blazor Photino.Blazor ...

  10. 从零开始使用 Astro 的实用指南

    在这个实用的Astro指南中,我将指导你完成设置过程,并告诉你如何构造你的文件.你将学习如何添加页面.交互式组件,甚至是markdown文章.我还会告诉你如何从服务器上获取数据,创建布局,并使用van ...