记一次 .NET 某医院预约平台 内存泄露分析

在我分析的真实dump案例中,见过 Castle ProxyGenerator​ 的泄露,也见过 CodeAnalysis.CSharp.Scripting​ 的泄露,还真没见过 XmlSerializer 的泄露,算是完美的补充了我的案例库!

一:背景

1. 讲故事

前几天有位朋友找到我,说他的程序有内存泄露,让我帮忙排查一下,截图如下:

图片


(相关资料图)

说实话看到 32bit, 1.5G 这些关键词之后,职业敏感告诉我,他这个可能是虚拟地址紧张所致,不管怎么说,有了 Dump 就可以上马分析。

二:WinDbg分析

1. 虚拟地址紧张所致吗

要看是不是虚拟地址紧张,可以用!address -summary观察下内存段统计信息,截图如下:

图片

我去,用 WinDbg Preview 尽然分析不了,在加载ntdll的过程中死掉了,如果你是我们调试训练营的朋友,应该会深深的有体会,我们分析的第一个dump就存在这个情况,这个加载不了其实就预示着一种非托管泄露,这里暂不剧透。

用WinDbg Preview分析不了怎么办呢?可以用 Windbg 的其他版本哈,比如Windbg10, WinDbg6等等,这里就采用WinDbg10 X86版本打开吧。

0:000> !address -summary--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotalFree                                    179          8cbb1000 (   2.199 GB)           54.97%Heap                                   6598          376f6000 ( 886.961 MB)  48.09%   21.65%                              3091          31954000 ( 793.328 MB)  43.02%   19.37%Image                                   376           8c0d000 ( 140.051 MB)   7.59%    3.42%Stack                                    75           1780000 (  23.500 MB)   1.27%    0.57%Other                                     7             4e000 ( 312.000 kB)   0.02%    0.01%TEB                                      25             19000 ( 100.000 kB)   0.01%    0.00%PEB                                       1              1000 (   4.000 kB)   0.00%    0.00%--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotalMEM_FREE                                179          8cbb1000 (   2.199 GB)           54.97%MEM_COMMIT                             9821          6bfad000 (   1.687 GB)  93.68%   42.18%MEM_RESERVE                             352           7492000 ( 116.570 MB)   6.32%    2.85%

从卦中MEM_COMMIT的%ofTotal= 42.18%来看,提交内存占总的虚拟地址比重还不到一半,这说明我的猜测是错的,不存在虚拟地址紧张的情况,这里稍微提醒一下的是,这里不存在虚拟地址紧张是因为它开的是Any CPU模式,默认能吃到 4G 内存。

不管怎么说,现在被当头一棒,既然这条路走不通,那会是什么情况导致的呢?一般来说这个内存量我是不愿意分析的,但既然分析到这里也只能继续分析,接下来用!eeheap -gc观察下托管堆内存占用情况。

0:000> !eeheap -gcNumber of GC Heaps: 1generation 0 starts at 0x777c0434generation 1 starts at 0x77781000generation 2 starts at 0x01861000ephemeral segment allocation context: none segment     begin  allocated      size01860000  01861000  0285ffdc  0xffefdc(16773084)...77780000  77781000  77aa25c0  0x3215c0(3282368)Large object heap starts at 0x02861000 segment     begin  allocated      size02860000  02861000  031e5cc0  0x984cc0(9981120)Total Size:              Size: 0x1f7e47e4 (528369636) bytes.------------------------------GC Heap Size:    Size: 0x1f7e47e4 (528369636) bytes.

从卦中看当前托管堆也才528M和 提交内存1.6G相距甚远,所以这个 dump 大概率是存在非托管内存泄露,其实!address -summary中的Heap也能佐证,说到底就是ntheap泄露。

2. ntheap 怎么啦

深挖 ntheap 我就不挖了,省的误入歧途,文章开头我说过 ntdll 无法加载的现象预示着一种非托管泄露,对 ,就是 GC 的加载堆泄露,加载堆是 CLR 用来映射 C# 程序集,模块,类型,方法等用途的一块私有内存,那怎么去洞察它呢?可以使用!eeheap -loader命令洞察。

0:000> !eeheap -loaderLoader Heap:--------------------------------------...Module 05829f78: Size: 0x0 (0) bytes.Module 0582a8f8: Size: 0x0 (0) bytes.Module 0582b278: Size: 0x0 (0) bytes.Module 0582bbf8: Size: 0x0 (0) bytes.Module 0582c578: Size: 0x0 (0) bytes.Module 0582cef8: Size: 0x0 (0) bytes.Module 0582d878: Size: 0x0 (0) bytes....Module 362ea420: Size: 0x0 (0) bytes.Total size:      Size: 0x0 (0) bytes.--------------------------------------Total LoaderHeap size:   Size: 0x7e7e000 (132636672) bytes total, 0x28000 (163840) bytes wasted.=======================================

虽然加载堆只统计到了132M,但其中的 module 高达2.3w个,其实这里会有一些相关内存是加载堆之外无法统计到的,一般正常的程序不可能有这么多的module,所以这就是我们接下来突破的点,那怎么突破呢?最好的办法就是观察下这个 module 中到底有什么 type,使用!dumpmodule命令即可。

0:000> !dumpmodule -mt 0582d878Name:       Unknown ModuleAttributes: Reflection Assembly:   0c229d38LoaderHeap:              00000000TypeDefToMethodTableMap: 050676e4TypeRefToMethodTableMap: 050676f8MethodDefToDescMap:      0506770cFieldDefToDescMap:       05067734MemberRefToDescMap:      00000000FileReferencesMap:       05067784AssemblyReferencesMap:   05067798Types defined in this module      MT  TypeDef Name------------------------------------------------------------------------------0582dcb0 0x02000002 0582df90 0x02000003 0582e018 0x02000004 0582e0b8 0x02000005 0582e194 0x02000006 Types referenced in this module      MT    TypeRef Name------------------------------------------------------------------------------

从模块中并没有看到类型的文字描述,那怎么办呢,我们随便抽一个 mt 看下这个 mt 下有什么方法,使用!dumpmt命令即可。

0:000> !dumpmt -md 0582dcb0EEClass:         05068980Module:          0582d878Name:            mdToken:         02000002File:            Unknown ModuleBaseSize:        0x44ComponentSize:   0x0Slots in VTable: 8Number of IFaces in IFaceMap: 0--------------------------------------MethodDesc Table   Entry MethodDe    JIT Name739819c8 735e61fc PreJIT System.Object.ToString()73987850 735e6204 PreJIT System.Object.Equals(System.Object)7398bd80 735e6224 PreJIT System.Object.GetHashCode()738ddbe8 735e6238 PreJIT System.Object.Finalize()0583b529 0582dc8c   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack.InitCallbacks()0583b52d 0582dc94   NONE Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack..ctor()0583c7d0 0582dc74    JIT Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack.Write3_root(System.Object)0583c868 0582dc80    JIT Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterCallBack.Write2_CallBack(System.String, System.String, xxx.Models.xxxBack, Boolean, Boolean)

看到卦中的这些信息,我相信有很多朋友知道是怎么回事了,对,就是Serialization泄露,那它序列化什么类型呢 ? 从卦中看就是xxx.Models.xxxBack类,即xmlSerializer.Serialize(xxx.Models.xxxBack)的相关逻辑,接下来就需要逆向看下到底是哪里写的,结果发现是他的底层库封装的,有些方法有问题,有些没问题,真的是无语哈。

//有问题的方法    public static string Serialize(object o, Encoding encoding, string rootName)    {        XmlSerializer xmlSerializer = new XmlSerializer(o.GetType(), new XmlRootAttribute(rootName));        ...        xmlSerializer.Serialize(memoryStream, o, xmlSerializerNamespaces);        return encoding.GetString(memoryStream.ToArray());    }    //正确的方法    public static string Serialize(object Obj, Encoding encoding)    {        ...        using (XmlWriter xmlWriter = XmlWriter.Create(memoryStream, xmlWriterSettings))        {            XmlSerializerNamespaces xmlSerializerNamespaces = new XmlSerializerNamespaces();            xmlSerializerNamespaces.Add("", "");            new XmlSerializer(Obj.GetType()).Serialize(xmlWriter, Obj, xmlSerializerNamespaces);        }        return encoding.GetString(memoryStream.ToArray());    }

这是一个老生常谈的问题,如果你用new XmlSerializer(o.GetType(), new XmlRootAttribute(rootName));模式的话,一定要缓存起来,否则就会泄露,只能说是微软造的一个大坑吧,多少人都踩上去了。

三:总结

在我分析的真实dump案例中,见过Castle ProxyGenerator的泄露,也见过CodeAnalysis.CSharp.Scripting的泄露,还真没见过XmlSerializer的泄露,算是完美的补充了我的案例库!

关键词:

    快讯

    浙江大学成立天文研究所,首任所长:康熙

    据浙江媒体都市快报消息,浙江大学7月4日发文成立天文研究所,康熙担任

    来源:今日科学 23-07-06

    中科磁业换手率70.38% 龙虎榜上机构买入863.31万元 卖出89.10万元

    中科磁业今日上涨%,全天换手率%,成交额亿元,振幅%。龙虎榜数据显示

    来源:证券时报网 23-07-06

    多措并举促六堡茶产业快速发展 环球快看点

    日前,在苍梧县六堡镇狮子山茶园,种下的六堡茶茶苗一片青绿,长势喜人

    来源:梧州日报 23-07-06

    52万债基今日收益136,2只鸡5个蛋,感觉不错-快资讯

    一、持仓概况目前债基持仓52万,主要持有债基21只,7月5日债基截止发稿

    来源:临沂炒鸡 23-07-06

    持续强化药品全生命周期质量监管

    持续强化药品全生命周期质量监管(主题)人民日报海外版北京7月5日电(

    来源:人民日报海外版 23-07-06
    返回顶部