Windbg系列之定位内存爆高代码
-
先写一个模拟程序,将内存拉高
public class TestService { public List<byte[]> Datas { get; set; } = new List<byte[]>(); public void Add() { for (int i = 0; i < 40000; i++) { Datas.Add(new byte[1024 * 1024]); } } } internal class Program { static TestService testService = new TestService(); static void Main(string[] args) { Console.WriteLine("Hello, World!"); testService.Add(); Debugger.Break(); Console.ReadLine(); } }
-
运行后,内存将持续升高,待内存稳定后,就可以开始我们的分析了。
使用Visual Studio Profiler 来简单查看爆高的原因
-
如果你拥有源码,且有VS开发环境,直接使用VS调试查看内存快照是最直接的方法
-
打开诊断工具
-
拍摄一个内存快照
-
分析内存快照
-
-
-
发现
TestService
中果然有一个List<byte[]>
类型的成员Datas
,接下来只要查看Datas
的所有引用即可 -
对于自己创建的类型,可以直接在快照中点击查看所有引用
Windbg 定位内存爆高原因
-
但是大多数情况下,我们没有源码,或者在生产环境下无法用vs调试,可以直接对内存高的进程抓取一个dump文件,然后传回自己的电脑分析
-
使用windbg打开dump文件之后,先用
!dumpheap -stat
查看托管内存信息 -
随便找到一个
byte[]
类型对象,查看它的引用根,这里我们就选最后一个吧:!gcroot 015163100240
0:011> !gcroot 015163100240 HandleTable: 000001474aae13e8 (strong handle) -> 01474b800028 System.Object[] -> 01474e009de8 LargeMemLocationSample.TestService -> 01474e009e00 System.Collections.Generic.List<System.Byte[]> -> 014750360780 System.Byte[][] -> 015163100240 System.Byte[] Found 1 unique roots.
-
可以看出,这个
byte[]
对象是TestServcie
的一个List<byte[]>
属性的成员如果这个时候我们有源码,可以查看TestService
的引用,如果没有源码,继续使用windbg往下定位 -
着重看一下这个
01474b800028 System.Object[]
,这是一个handle表,代码里所有的引用都是来自 handle 表,要想查找TestService
的引用,先了解一下A
和B
地址的概念 -
接下来我们先在 Handle表中寻找内容为B地址的A地址
0:011> !dumpobj /d 1474b800028 Name: System.Object[] MethodTable: 00007ffadff6c4d8 EEClass: 00007ffadff6c440 Tracked Type: false Size: 8184(0x1ff8) bytes Array: Rank 1, Number of elements 1020, Type CLASS (Print Array) Fields: None 0:011> s -q 01474b800028 L?0x1ff8 01474e009de8 00000147`4b801d68 00000147`4e009de8 00000147`4e009790
!do 1474b800028
是为了查看handle表整个大小是多少s -q 01474b800028 L?0x1ff8 01474e009de8
是在handle表中找b地址s
表示搜索内存-q
表示安静搜索01474b800028
表示搜索起始地址L?0x1ff8
表示搜索的长度 将会在[01474b800028, 01474b800028 + 1ff8]
中搜索01474e009de8
要搜索的内容
- 可以得出 A 地址为
000001474b801d68
-
现在我们得到了A地址,接下来就要查找所有引用A地址的地方,使用的方法也很简单、粗暴,搜索所有的内存,查看内容为
000001474b801d68
的地方,然后查看它的反汇编0:011> s -q 01474b800028 L?0x1ff8 01474e009de8 00000147`4b801d68 00000147`4e009de8 00000147`4e009790 0:011> s -b 0 L?ffffffff`ffffffff 68 1d 80 4b 47 01 00000147`4b0d197b 68 1d 80 4b 47 01 00 00-48 8b 09 39 09 ff 15 3a h..KG...H..9...: 00000147`4b0d1a02 68 1d 80 4b 47 01 00 00-e8 01 e6 e8 ff 90 48 83 h..KG.........H. 00007ffa`e003197b 68 1d 80 4b 47 01 00 00-48 8b 09 39 09 ff 15 3a h..KG...H..9...: 00007ffa`e0031a02 68 1d 80 4b 47 01 00 00-e8 01 e6 e8 ff 90 48 83 h..KG.........H. 00007ffa`e00be498 68 1d 80 4b 47 01 00 00-01 00 00 00 00 00 00 00 h..KG...........
- 直接从
7ffa
开头的地址看,!ip2md 00007ffae003197b
使用以上命令查找所有的引用之处
0:011> !ip2md 00007ffa`e003197b MethodDesc: 00007ffae00e00d0 Method Name: LargeMemLocationSample.Program.Main(System.String[]) Class: 00007ffae00cfbb8 MethodTable: 00007ffae00e0108 mdToken: 0000000006000001 Module: 00007ffae00be0a0 IsJitted: yes Current CodeAddr: 00007ffae0031930 Version History: ILCodeVersion: 0000000000000000 ReJIT ID: 0 IL Addr: 000001474b212050 CodeAddr: 00007ffae0031930 (MinOptJitted) NativeCodeVersion: 0000000000000000 Source file: D:\files\blend\git pros\gitee\blog-codes\dotnet\DebugSamples\LargeMemLocationSample\Program.cs @ 11
-
从结果来看
Program.cs
的第11行,使用了TestService
-
如果没有源码,可先导出module(导出一个dll)再通过
ILSpy
反编译0:011> !savemodule 00007ffae00be0a0 d:\test.dll 3 sections in file section 0 - VA=2000, VASize=a70, FileAddr=200, FileSize=c00 section 1 - VA=4000, VASize=630, FileAddr=e00, FileSize=800 section 2 - VA=6000, VASize=c, FileAddr=1600, FileSize=200
- 直接从
-
summary
- 对于内存爆高问题解决方案:
- 如果有源码有vs环境,直接使用vs profiler 进行分析
- 如果有源码没有开发环境,使用windbg分析dump文件,定位内存爆高的类型,再用源码查找引用
- 如果没有源码,使用windbg分析dump文件,定位内存爆高的类型,再用windbg查找引用的函数,再导出dll使用ILSpy反编译查看函数源码