首先声明的洞察是这个 黑洞 是我定义的术语,它是管堆用来表示 内存吞噬 的一种现象,何为 内存吞噬,内存我们来看一张图。黑洞现象
图片
从上面的洞察 卦象图 来看,GCHeap 的管堆 Allocated=852M 和 Committed=16.6G,它们的内存差值就是 分配缓冲区=16G,缓冲区的黑洞现象好处就是用空间换时间,弊端就是洞察会实实在在的侵占内存,挤压其他程序的管堆生存空间。
万事皆有因果,今生的果是前世种的因,换句话说是程序曾经有大量及频繁的创建临时对象,让GC不自主的痉挛,小挛伤神,大挛伤身,所以GC为了避免大挛的发生,就大量的囤积本应该释放掉的内存,目的就是防止未来某个时刻再次有大内存分配的发生。
我相信因果关系大家都弄清楚了,但口说无凭,还得用代码证明一下不是?为了模拟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 23351c3aab8generation 1 starts at 233484c38e0generation 2 starts at 233484c1000ephemeral segment allocation context: noneSmall 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表现不管在 道德 还是 伦理 上都说不通的。
要想找到前世的因,手段有很多,比如用 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() 造的孽,至此真相大白。
化解之道有很多:
简而言之就是将 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次。
责任编辑:武晓燕 来源: 一线码农聊技术 CLR优化空间(责任编辑:探索)
产品供不应求叠加成本推升 钛白粉国内涨价幅度800元/吨至1000元/吨
特斯拉一季度在华交付“1600多台”是谣传?特斯拉不承认存在“库存”
金砖国家新开发银行与巴西签署首份针对巴西的贷款协议 总额为3亿美元
中国煤层气(08270.HK)年度亏损收窄至3622.4万元 每股亏损为人民币3.08分
产品供不应求叠加成本推升 钛白粉国内涨价幅度800元/吨至1000元/吨
特斯拉一季度在华交付“1600多台”是谣传?特斯拉不承认存在“库存”
牛年首个交易日三大股指全线跳空高开 四现象折射市场资金调仓换股
桂发祥(002820.SZ)2020年度净利润降70.41% 基本每股收益0.12元