一:背景

1. 讲故事

前段时间微信上有一位老朋友找到我,说他的程序跑着跑着内存会突然爆高,有时候会下去,有什么会下不去,怀疑是不是某些情况下存在内存泄露,让我帮忙分析一下,其实内存泄露方面的问题还是比较好解决的,看过这个dump之后觉得还是有一定的分享价值,拿出来和大家分享一下吧。

二:WinDbg 分析

1. 托管还是非托管泄露

这个首先是一定要确定的,否则就是南辕北辙,强调再多也不为过,可以用 !address -summary 观察一下。


0:000> !address -summary --- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free 208 7ffb`691cd000 ( 127.982 TB) 99.99%
<unknown> 384 4`898d7000 ( 18.149 GB) 98.87% 0.01%
Image 1015 0`0b053000 ( 176.324 MB) 0.94% 0.00%
Stack 90 0`01700000 ( 23.000 MB) 0.12% 0.00%
Heap 32 0`00bea000 ( 11.914 MB) 0.06% 0.00%
Other 14 0`001e0000 ( 1.875 MB) 0.01% 0.00%
TEB 23 0`0002e000 ( 184.000 kB) 0.00% 0.00%
PEB 1 0`00001000 ( 4.000 kB) 0.00% 0.00% --- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_PRIVATE 336 4`8ab0b000 ( 18.167 GB) 98.96% 0.01%
MEM_IMAGE 1177 0`0b70f000 ( 183.059 MB) 0.97% 0.00%
MEM_MAPPED 46 0`00c09000 ( 12.035 MB) 0.06% 0.00% --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 208 7ffb`691cd000 ( 127.982 TB) 99.99%
MEM_COMMIT 1256 3`1f910000 ( 12.493 GB) 68.05% 0.01%
MEM_RESERVE 303 1`77513000 ( 5.864 GB) 31.95% 0.00%

从卦中看,当前程序的提交内存占用了 12G,NTHEAP 占用了 11M,基本上就可以断定这是一个托管内存的问题,看到这里还是非常开心的,由于托管层都带有类型数据,分析起来非常简单,接下来使用 !eeheap -gc 观察下托管堆。


0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00000000e42f26c8
generation 1 starts at 0x00000000e42e85a0
generation 2 starts at 0x0000000001d71000
ephemeral segment allocation context: none
segment begin allocated size
0000000001d70000 0000000001d71000 0000000011961b98 0x000000000fbf0b98(264178584)
000000005e540000 000000005e541000 000000006c2b6408 0x000000000dd75408(232215560)
0000000247ff0000 0000000247ff1000 0000000252d751f8 0x000000000ad841f8(181944824)
00000000e3ff0000 00000000e3ff1000 00000000f1e44850 0x000000000de53850(233125968)
Large object heap starts at 0x0000000011d71000
segment begin allocated size
0000000011d70000 0000000011d71000 0000000019d65098 0x0000000007ff4098(134168728)
000000001dad0000 000000001dad1000 0000000025ac6fd0 0x0000000007ff5fd0(134176720)
0000000028c50000 0000000028c51000 0000000030c318e8 0x0000000007fe08e8(134088936)
0000000030c50000 0000000030c51000 0000000038c35518 0x0000000007fe4518(134104344)
000000003e540000 000000003e541000 0000000046507ff0 0x0000000007fc6ff0(133984240)
0000000046540000 0000000046541000 000000004e5296d0 0x0000000007fe86d0(134121168)
000000004e540000 000000004e541000 000000005652af00 0x0000000007fe9f00(134127360)
0000000056540000 0000000056541000 000000005e50b050 0x0000000007fca050(133996624)
...
000000058fff0000 000000058fff1000 00000005d76c2910 0x00000000476d1910(1198332176)
000000064e820000 000000064e821000 0000000695cbe808 0x000000004749d808(1196021768)
000000013bff0000 000000013bff1000 00000001834c96d8 0x00000000474d86d8(1196263128)
0000000257ff0000 0000000257ff1000 000000029f44e7e0 0x000000004745d7e0(1195759584)
Total Size 0x2dbe1b140(12278935872)
------------------------------
GC Heap Size 0x2dbe1b140(12278935872)

从卦中看,托管堆占用了 12G 内存,而且都是被 LOH 给吃掉了,离真相越来越近了,接下来就是用 !dumpheap -stat 观察下托管堆,看下到底是什么类型占的这么大?


0:000> !dumpheap -stat
total 477283 objects
Statistics:
MT Count TotalSize Class Name
0007ffeb28280b0 3516 365664 System.Reflection.RuntimeMethodInfo
00007ffea909df50 62 728528 System.Data.RBTree`1+Node[[System.Data.DataRow, System.Data]][]
00007ffeb285a610 14 804680 System.DateTime[]
00007ffea909c5e8 17061 1637856 System.Data.DataRow
00007ffeb2831180 1555 1978136 System.Int32[]
00007ffeb282d430 1108 6648296 System.Int64[]
00007ffeb2fce9d8 71 12821784 System.Decimal[]
00007ffeb282a060 366739 15054680 System.String
00007ffeb2817e50 7785 23534144 System.Object[]
00000000009a2e60 4084 4772737632 Free
00007ffeb28320a0 17400 7438877632 System.Byte[]
...

从卦中看, System.Byte[] 数组只有 1.7W 个,居然占用了 7.4G 的内存,这说明有的 Byte[] 可能会非常大, 接下来就可以检索 >10M 的数组排列情况,输出如下:


0:000> !dumpheap -mt 00007ffeb28320a0 -min 0n10485760
Address MT Size
0000000029051018 00007ffeb28320a0 33554456
000000002d051078 00007ffeb28320a0 33554456
00000003e6a43a10 00007ffeb28320a0 16777240
0000000547ff1000 00007ffeb28320a0 1195710200
000000058fff1000 00007ffeb28320a0 1195710200
000000064e821000 00007ffeb28320a0 1195759552
000000013bff1000 00007ffeb28320a0 1195759552
0000000257ff1000 00007ffeb28320a0 1195759560
total 8 objects
Statistics:
MT Count TotalSize Class Name
00007ffeb28320a0 8 6062585216 System.Byte[]
Total 8 objects

从卦中看,居然有 5 个将近 1.2 G 的 Byte[] 数组,有点吓人,抽几个看看吧。


0:000> !gcroot 000000058fff1000
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 39f4 0:000> !gcroot 0000000547ff1000
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 39f4
Scan Thread 2 OSTHread 2f6c
Scan Thread 3 OSTHread 3354
Scan Thread 4 OSTHread 3924

从卦中看,他们都没有引用根,也就说明是等待 GC 回收的临时对象,有朋友就要问了,GC 为什么不回收呢?

2. GC 为什么不回收

了解 GC 的朋友应该知道,LOH 默认不启用压缩回收,只会做简单的标记清除,清除之后就会存在很多的空洞,只有相邻的空洞才会合并成一个更大的空洞,一旦有分配的对象超过所有的空洞大小,GC 也只能被迫commit更多的新空间给它存放,虽然此时存放不间隔的空闲内存已经足够。

如果大家有点懵,画个图大概就像下面这样。

从图中看,程序要分配 1.3G 的对象, 虽然 LOH 上有 2G 的空闲空间但就是放不下,无奈只能 commit 更多的内存来存放这个对象。

这就是为什么朋友看到的,有时候内存会暴涨,有时候会降下去的底层原理。

3. byte[] 到底是什么

要想查看 byte[] 的内容,可以用 db, dW 观察内存地址,参考如下:


0:000> dW 0000000547ff1000 L100
00000005`47ff1000 20a0 b283 7ffe 0000 16da 4745 0000 0000 . ........EG....
00000005`47ff1010 0100 0000 ff00 ffff 01ff 0000 0000 0000 ................
00000005`47ff1020 0c00 0002 0000 534e 7379 6574 2e6d 6144 ......NSystem.Da
00000005`47ff1030 6174 202c 6556 7372 6f69 3d6e 2e32 2e30 ta, Version=2.0.
00000005`47ff1040 2e30 2c30 4320 6c75 7574 6572 6e3d 7565 0.0, Culture=neu
00000005`47ff1050 7274 6c61 202c 7550 6c62 6369 654b 5479 tral, PublicKeyT
00000005`47ff1060 6b6f 6e65 623d 3737 3561 3563 3136 3339 oken=b77a5c56193
00000005`47ff1070 6534 3830 0539 0001 0000 5315 7379 6574 4e089......Syste
00000005`47ff1080 2e6d 6144 6174 442e 7461 5461 6261 656c m.Data.DataTable
00000005`47ff1090 04d6 0000 4419 7461 5461 6261 656c 522e .....DataTable.R
00000005`47ff10a0 6d65 746f 6e69 5667 7265 6973 6e6f 4418 emotingVersion.D
00000005`47ff10b0 7461 5461 6261 656c 522e 6d65 746f 6e69 ataTable.Remotin
00000005`47ff10c0 4667 726f 616d 1374 6144 6174 6154 6c62 gFormat.DataTabl
00000005`47ff10d0 2e65 6154 6c62 4e65 6d61 1365 6144 6174 e.TableName.Data
00000005`47ff10e0 6154 6c62 2e65 614e 656d 7073 6361 1065 Table.Namespace.
00000005`47ff10f0 6144 6174 6154 6c62 2e65 7250 6665 7869 DataTable.Prefix
00000005`47ff1100 4417 7461 5461 6261 656c 432e 7361 5365 .DataTable.CaseS
00000005`47ff1110 6e65 6973 6974 6576 441e 7461 5461 6261 ensitive.DataTab
00000005`47ff1120 656c 632e 7361 5365 6e65 6973 6974 6576 le.caseSensitive
00000005`47ff1130 6d41 6962 6e65 1474 6144 6174 6154 6c62 Ambient.DataTabl
00000005`47ff1140 2e65 6f4c 6163 656c 434c 4449 4419 7461 e.LocaleLCID.Dat
00000005`47ff1150 5461 6261 656c 4d2e 6e69 6d69 6d75 6143 aTable.MinimumCa
00000005`47ff1160 6170 6963 7974 4419 7461 5461 6261 656c pacity.DataTable
00000005`47ff1170 4e2e 7365 6574 4964 446e 7461 5361 7465 .NestedInDataSet
00000005`47ff1180 4412 7461 5461 6261 656c 542e 7079 4e65 .DataTable.TypeN
00000005`47ff1190 6d61 1b65 6144 6174 6154 6c62 2e65 6552 ame.DataTable.Re
00000005`47ff11a0 6570 7461 6261 656c 6c45 6d65 6e65 1c74 peatableElement.
00000005`47ff11b0 6144 6174 6154 6c62 2e65 7845 6574 646e DataTable.Extend
00000005`47ff11c0 6465 7250 706f 7265 6974 7365 4417 7461 edProperties.Dat
00000005`47ff11d0 5461 6261 656c 432e 6c6f 6d75 736e 432e aTable.Columns.C
00000005`47ff11e0 756f 746e 4421 7461 5461 6261 656c 442e ount!DataTable.D
00000005`47ff11f0 7461 4361 6c6f 6d75 5f6e 2e30 6f43 756c ataColumn_0.Colu
....

从 unicode 码来看,貌似是一个 DataTable 的序列化,看样子是序列化一个巨大的 DataTable 导致的内存暴涨,接下来我们到线程栈上找到有么有类似的操作,算是碰碰运气吧。

哈哈,从卦中看还真的有类似操作,用了 BinaryFormatter 做序列化。。。接下来观察下这个 DataTable 的 RowCount。


0:013> !do 0000000002582cb8
Name: System.Data.DataRowCollection+DataRowTree
MethodTable: 00007ffea909d3f0
EEClass: 00007ffea8f1ef68
Size: 64(0x40) bytes
(C:\Windows\assembly\GAC_64\System.Data\2.0.0.0__b77a5c561934e089\System.Data.dll)
Fields:
MT Field Offset Type VT Attr Value Name
0000000000000000 4000765 8 SZARRAY 0 instance 000000000bc79d98 _pageTable
00007ffeb2831180 4000766 10 System.Int32[] 0 instance 000000000bc79fb8 _pageTableMap
00007ffeb28312d0 4000767 18 System.Int32 1 instance 42 _inUsePageCount
00007ffeb28312d0 4000768 1c System.Int32 1 instance 1 nextFreePageLine
00007ffeb28312d0 4000769 20 System.Int32 1 instance 1245312 root
00007ffeb28312d0 400076a 24 System.Int32 1 instance 17054 _version
00007ffeb28312d0 400076b 28 System.Int32 1 instance 17055 _inUseNodeCount
00007ffeb28312d0 400076c 2c System.Int32 1 instance 0 _inUseSatelliteTreeCount
00007ffea957b578 400076d 30 System.Int32 1 instance 2 _accessMethod

从卦中看当前的 _inUseNodeCount=1.7W ,这只是捕获到的,相信还有更大的,也算是找到问题原因了。

三:总结

在和朋友沟通之后,朋友也确认了确实有大 DataTable 的情况,目前业务需求没法绕过,不过在这种情况下也还是有办法的,大致如下:

1. 强制 LOH 压缩

虽然 LOH 上的对象移动会产生很大的内存流量,但适当的用一用压缩也不失为一个简单粗暴的办法。


GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();

2. 分批次获取

一次性获取 1.7W 条,可以拆成诸如 2000 条一次,好处很明显,可以充分利用 LOH 上留下来的空洞区。

记一次 .NET某家装ERP系统 内存暴涨分析的更多相关文章

  1. 记一次 .NET 某三甲医院HIS系统 内存暴涨分析

    一:背景 1. 讲故事 前几天有位朋友加wx说他的程序遭遇了内存暴涨,求助如何分析? 和这位朋友聊下来,这个dump也是取自一个HIS系统,如朋友所说我这真的是和医院杠上了,这样也好,给自己攒点资源, ...

  2. 记一次 .NET 某WMS仓储打单系统 内存暴涨分析

    一:背景 1. 讲故事 七月中旬有一位朋友加wx求助,他的程序在生产上跑着跑着内存就飙起来了,貌似没有回头的趋势,询问如何解决,截图如下: 和这位朋友聊下来,感觉像是自己在小县城当了个小老板,规律的生 ...

  3. 记一次 .NET医疗布草API程序 内存暴涨分析

    一:背景 1. 讲故事 我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS ...

  4. 记一次 .NET 某招聘网后端服务 内存暴涨分析

    一:背景 1. 讲故事 前段时间有位朋友wx找到我,说他的程序存在内存阶段性暴涨,寻求如何解决,和朋友沟通下来,他的内存平时大概是5G 左右,在某些时点附近会暴涨到 10G+, 画个图大概就是这样. ...

  5. 记一次 .NET 医院CIS系统 内存溢出分析

    一:背景 1. 讲故事 前几天有位朋友加wx求助说他的程序最近总是出现内存溢出,很崩溃,如下图: 和这位朋友聊下来,发现他也是搞医疗的,哈哈,.NET 在医疗方面还是很有市场的,不过对于内存方面出的问 ...

  6. 记一次 .NET 某智能服装智造系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友找到我,说他的程序出现了内存泄漏,不知道如何进一步分析,截图如下: 朋友这段话已经说的非常言简意赅了,那就上 windbg 说话吧. 二:Windbg 分析 1. ...

  7. 记一次 WinDbg 分析 .NET 某工厂MES系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友加微信求助,说他的程序跑着跑着就内存爆掉了,寻求如何解决,截图如下: 从聊天内容看,这位朋友压力还是蛮大的,话说这貌似是我分析的第三个 MES 系统了,看样子 . ...

  8. 记一次 .NET 某消防物联网 后台服务 内存泄漏分析

    一:背景 1. 讲故事 去年十月份有位朋友从微信找到我,说他的程序内存要炸掉了...截图如下: 时间有点久,图片都被清理了,不过有点讽刺的是,自己的程序本身就是做监控的,结果自己出了问题,太尴尬了 二 ...

  9. 记一次 .NET 某电厂Web系统 内存泄漏分析

    一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...

  10. 记一次 .NET 某智慧物流 WCS系统 CPU 爆高分析

    一:背景 1. 讲故事 哈哈,再次见到物流类软件,上个月有位朋友找到我,说他的程序出现了 CPU 爆高,让我帮忙看下什么原因,由于那段时间在苦心研究 C++,分析和经验分享也就懈怠了,今天就给大家安排 ...

随机推荐

  1. 【SQL基础】多表查询:子查询、连接查询(JOIN)、组合查询(UNION集合运算)

    〇.概述 1.内容 JOIN表连接(内连接INNER JOIN/JOIN)(外连接LEFT/RIGHT (OUTER) JOIN) 集合运算-UNION联合 2.建表语句 drop table if ...

  2. 【离线数仓】Day02-用户行为数据仓库:分层介绍、环境搭建(hive、tez)、LZO压缩、建表查询导入加索引、编写脚本

    一.数仓分层概念 1.为什么要分层 ODS:原始数据层 DWD层:明细数据层 DWS:服务数据层 ADS:数据应用层 2.数仓分层 3.数据集市与数据仓库概念 4.数仓命名规范 ODS层命名为odsD ...

  3. keras小点记录

    Keras学习小点记录 1.axis(轴) (1)解释 参考链接:https://www.zhihu.com/question/58993137 (2)测试 参考链接:http://keras-cn. ...

  4. 【JVM故障问题排查心得】「内存诊断系列」Docker容器经常被kill掉,k8s中该节点的pod也被驱赶,怎么分析?

    背景介绍 最近的docker容器经常被kill掉,k8s中该节点的pod也被驱赶. 我有一个在主机中运行的Docker容器(也有在同一主机中运行的其他容器).该Docker容器中的应用程序将会计算数据 ...

  5. 多表查询两种方法、可视化软件navicat、python操作mysql、pymysql模块

    目录 多表查询的思路 多表查询的两种方法 小知识点补充数说明 可视化软件Navicat 安装教程 数据库常用操作 多表查询练习题 python 操作MySQL pymysql补充说明 Non-grou ...

  6. 深入解读MySQL InnoDB存储引擎Update语句执行过程

    参考b站up 戌米的论文笔记 https://www.bilibili.com/video/BV1Tv4y1o7tA/ 书籍<mysql是怎样运行的> 极客时间<mysql实战45讲 ...

  7. python 中变量的命名规则与注释

    变量命名规则 1.变量名必须是大小写英文字母.数字或下划线 _ 的组合,不能用数字开头,并且对大小写敏感 2.关键字不能用于命名变量,关键字一共有35个,以下为关键字的获取 注释 代码注释提高了代码的 ...

  8. [编程基础] Python装饰器入门总结

    Python装饰器教程展示了如何在Python中使用装饰器基本功能. 文章目录 1 使用教程 1.1 Python装饰器简单示例 1.2 带@符号的Python装饰器 1.3 用参数修饰函数 1.4 ...

  9. Java学习笔记:2022年1月9日(其二)

    Java学习笔记:2022年1月9日(其二) 摘要:这篇笔记主要记录了1月9日学习的第四章的类的基础知识,以及访问器以及访问器于多线程的意义. 目录 Java学习笔记:2022年1月9日(其二) 1. ...

  10. Python 常用库函数

    压缩 zlib: 兼容gzip的压缩 gzip: 对gzip文件的支持 bz2: 对bzip2压缩的支持 lzma: 使用LZMA算法的压缩 zipfile: 操作ZIP存档 tarfile: 读取t ...