排查 32 位进程的内存泄漏:当 OOM 报错时
System.OutOfMemoryException
at System.Text.Encoding.GetChars(Byte[] bytes, Int32 index, Int32 count)
at System.Text.Encoding.Convert(...)
at .SQLSupport.Extensions.ToByteArray(String s)
at .SQLSupport.Helper.Serializer.ToDataBuffer(IEntity entity)
...最离谱的是,报错虽然是 OutOfMemory,但当时服务器的物理内存稳如泰山,还有几十 GB 没动呢。
我一开始以为是数据“中毒”了,觉得是不是哪条脏数据在做字符串序列化时,算出了一个天文数字般的长度,导致程序尝试申请好几个 GB 的内存直接崩掉。但排查了一圈发现,报错的字段和方法完全随机,甚至连登录 Session 阶段都会触发,这显然不是某条数据的问题,而是整个进程已经“病入膏肓”了。
后来我意识到一个关键点:这玩意儿是个 32 位的进程。在 .NET 里,32 位进程的地址空间上限就被锁死在 2GB 左右,就算你给它配 128GB 的内存,它也只能看到那 2GB。
但我盯着任务管理器看的时候,发现那个进程占用的内存才 140MB 左右,完全没到 2GB 的天花板啊?这逻辑根本对不上。
既然不能直接挂 Profiler(毕竟是闭源软件,不敢乱动),我只能采取最原始但也最有效的“蹲点监控”法。我写了个简单的脚本,每小时定时记录一下该服务账号下进程的 Commit Size(提交大小)和 Handle Count(句柄数)。
我盯着导出的 CSV 数据看了一整天,终于抓到了那个“幽灵”:
Time CommitMB Handles
10:00 293 2889
11:00 387 3779
12:00 485 4650
... ... ...
21:00 1361 13429
22:00 1451 14361曲线图极其诡异,内存和句柄数几乎是成比例地直线飙升。真相大白了:系统里有一个 32 位的 COM+ 组件在后台疯狂泄露内存和句柄,速度大约是每小时 96MB。
这根本不是什么数据问题,也不是硬件资源不够,而是由于这种缓慢的泄露,导致进程的虚拟地址空间被一点点蚕食殆尽。当它接近那个不可见的 2GB 临界点时,任何哪怕很小的内存申请都会触发 OutOfMemoryException。
这次实战也给我提了个醒,在处理这类老旧集成项目时,别只盯着数据看,有时候“环境”本身就是最大的变量。