一:背景
1. 讲故事
前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享一下。
二:WinDbg 分析
1. 托管还是非托管泄漏
这个还是老规矩 !address -summary 和 !eeheap -gc 组合命令排查一下。- 0:000> !address -summary
-
- Mapping file section regions...
- Mapping module regions...
- Mapping PEB regions...
- Mapping TEB and stack regions...
- Mapping heap regions...
- Mapping page heap regions...
- Mapping other regions...
- Mapping stack trace database regions...
- Mapping activation context regions...
- --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
- MEM_FREE 426 7df8`af1ce000 ( 125.971 TB) 98.42%
- MEM_RESERVE 619 206`01b9c000 ( 2.023 TB) 99.75% 1.58%
- MEM_COMMIT 3096 1`4f286000 ( 5.237 GB) 0.25% 0.00%
- 0:000> !eeheap -gc
- Number of GC Heaps: 16
- ------------------------------
- ...
- Heap 15 (0000024AF6BAA2E0)
- generation 0 starts at 0x000002509729B538
- generation 1 starts at 0x000002509720B638
- generation 2 starts at 0x0000025096F91000
- ephemeral segment allocation context: none
- segment begin allocated committed allocated size committed size
- 0000025096F90000 0000025096F91000 000002509B5AFB40 000002509DFE9000 0x461eb40(73526080) 0x7058000(117800960)
- Large object heap starts at 0x00000250D6F91000
- segment begin allocated committed allocated size committed size
- 00000250D6F90000 00000250D6F91000 00000250DEB6AC60 00000250DEB6B000 0x7bd9c60(129866848) 0x7bda000(129867776)
- Pinned object heap starts at 0x00000250E6F91000
- 00000250E6F90000 00000250E6F91000 00000250E75D94E0 00000250E75DA000 0x6484e0(6587616) 0x649000(6590464)
- Allocated Heap Size: Size: 0xc840c80 (209980544) bytes.
- Committed Heap Size: Size: 0xec32000 (247668736) bytes.
- ------------------------------
- GC Allocated Heap Size: Size: 0xd6904dd8 (3599781336) bytes.
- GC Committed Heap Size: Size: 0x11884b000 (4706316288) bytes.
复制代码 从卦中指标看:5.2G 和 4.7G ,很明显问题出在了托管层,但如果你细心的话,你会发现这 4.7G 是 commit 内存,其实真正占用的只有 3.5G,言外之意有 1.2G 的空间其实属于 Commit 区,也就是为了少向 OS 申请内存而虚占的一部分空间,画个简图就像下面这样:

这也是我第一次看到 Alloc 和 Commit 差距有这么大。
2. 探究托管内存占用
首先看下 3.5G 内存这块,这个分析比较简单,直接看托管堆就好了。- 0:000> !dumpheap -stat
- Statistics:
- MT Count TotalSize Class Name
- ...
- 00007ffa19e64808 25804 36125600 xxxx.MongoDB.Entity.GeneratorMongodb
- 0000024af68aa2c0 20517 630474976 Free
- 00007ffa1947bf30 52477 654558722 System.Byte[]
- 00007ffa194847f0 1921 1044818774 System.Char[]
- 00007ffa19437a90 673850 1116597742 System.String
复制代码 从输出信息看,主要还是被 String,Char[],Byte[] 占用了,根据经验,这三个组合在一块,大多是存了什么字节流在内存中,比如 Pdf,Image ,然后在内存中倒来倒去就成这个样子了。
接下来在 char[] 中抽一些 obj 看一下,果然大多是 jpg 。- 0:000> !DumpObj /d 00000250da9d3618
- Name: System.Char[]
- MethodTable: 00007ffa194847f0
- EEClass: 00007ffa19484770
- Size: 11990052(0xb6f424) bytes
- Array: Rank 1, Number of elements 5995014, Type Char (Print Array)
- Content: data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAA4QAAASwCAYAAACjAoQOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DA
- Fields:
- None
- 0:000> !DumpObj /d 00000250db542a60
- Name: System.Char[]
- MethodTable: 00007ffa194847f0
- EEClass: 00007ffa19484770
- Size: 15667860(0xef1294) bytes
- Array: Rank 1, Number of elements 7833918, Type Char (Print Array)
- Content: data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAA4QAAASwCAYAAACjAoQOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DA
- Fields:
- None
复制代码 可以看到,3.2G 的内存大多是被 图片 所占用,朋友反馈是把 图片 存到数据库所致,好了,这一块就分析到这里,分析思路也很明显,接下来探究下 alloc 和 commit 的问题。
3. 为什么 alloc 和 commit 差距这么大
一般而言,差距大有以下几点诱因所致。
- segment 越大,commit 预设的区域就越大
根据官方文档的定义,segment 的大小取决于 cpu核数 和 程序的位数,截图如下:

有了这个指标,怎么到 dump去找各自数据呢,用 !eeversion 看下 heap 的个数以及观察下内存地址的长度就好啦。- 0:000> !eeversion
- 5.0.621.22011 free
- 5,0,621,22011 @Commit: 478b2f8c0e480665f6c52c95cd57830784dc9560
- Server mode with 16 gc heaps
- SOS Version: 6.0.5.7301 retail build
复制代码 可以看到,这个程序是用 64bit 跑在 16 核机器上,segment 上限为 1G 。
- segment 越多,alloc 和 commit 累计差距就会越大
每个 segment 都差一点,那多个 segment 自然就累计出来了,接下来就找一下那些差距比较大的 segment。- Heap 0 (0000024AF685A500)
- segment begin allocated committed allocated size committed size
- 0000024AF6F90000 0000024AF6F91000 0000024AF83B6D28 0000024AFEB42000 0x1425d28(21126440) 0x7bb1000(129699840)
- ------------------------------
- Heap 1 (0000024AF68819A0)
- segment begin allocated committed allocated size committed size
- 0000024B56F90000 0000024B56F91000 0000024B58507410 0000024B5D2E5000 0x1576410(22504464) 0x6354000(104153088)
- ------------------------------
- Heap 4 (0000024AF688F770)
- segment begin allocated committed allocated size committed size
- 0000024C76F90000 0000024C76F91000 0000024C783BDBE8 0000024C7ECF7000 0x142cbe8(21154792) 0x7d66000(131489792)
- ------------------------------
- Heap 6 (0000024AF68980A0)
- segment begin allocated committed allocated size committed size
- 0000024D36F90000 0000024D36F91000 0000024D38B87E78 0000024D3F881000 0x1bf6e78(29322872) 0x88f0000(143589376)
- ...
复制代码 从输出信息看,差距最大的是 Heap6,高达 110M,那这 110M 差距是否合理呢?其实仔细想想也不太离谱,毕竟命中了上面提到的两点,但我觉得这里的空间是不是还可以再智能的优化一下,再缩小一点?
4. Commit区能不能再小点?
能不能缩的再小一点,其实这是一种 CLR 智能算法的抉择,Commit 区越大,申请对象的速度就越快,向 OS 申请内存的频率就越低,反之 Commit 区越小,向 OS 再次申请内存的概率就越大,段的模型图大概是这个样子:

后来仔细想了下,既然 Commit 区多保留了 110M,那曾经肯定是某一个时刻突破过,后来因为成了垃圾对象,被 GC 回收了,但内存区域被GC私藏下来,所以程序肯定出现过 快出快进 的现象,接下来的想法就是用 writemem 把 alloc ~ commit 的内存区间给导出来看下,是不是有什么新发现。- 0:000> .writemem D:\dumps\dump1\1.txt 0000024AF83B6D28 L?0x0678b2d8
- Writing 678b2d8 bytes.............
复制代码
发现了很多类似这样的信息,把这个信息提供给朋友后,朋友说他找到这块问题了,是网站上用 NPOI 数据导出 功能所致。
三:总结
其实这个 dump 给了我们两方面的教训。
- 不要将 image 放到 sqlserver 里,不仅占用sql的资源,让程序也不堪重负,毕竟读出去都是 byte[] ...
- coreclr 虽然有自己的抉择算法,如果再智能一点就好了,让 commit ~ alloc 之间的差距更合理一点。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |