原文来自:http://blogs.msdn.com/b/tess/archive/2008/02/20/net-debugging-demos-lab-3-memory-review.aspx
1. 打开Performance Monitor。在Counter Logs里添加New Log Settings,命名为Lab3-Mem。添加的Log对象为.NET CLR Memory和Process。
2. 使用TinyGet执行命令个 tinyget –srv:localhost –uri:/buggybits/links.aspx –loop:4000
3. 命令执行以后,可以看到w3wp.exe进程的大小持续增长,并且在tinyeget命令结束以后,程序所站大小仍然不减:
4. 等待一段时间以后,在Performance monitor里面打开刚才存储的日志,添加Process对象的Virtual bytes和Private Bytes计数器,添加.NET CLR Memory对象的Bytes in all Heaps计数器,将他们的scale调整为0.00000001:
可见随着Bytes in All heaps(紫红)的增长,进程的virtual bytes(白色)和Private Bytes(青色)也在增长。因此可以判定是.NET进程的heap的增长带来了内存消耗的增长,并且内存增长之后没有回收。
5. 切换到WinDbg目录在命令行执行 adplus –hang –pn w3wp.exe –quiet 注意此时应该将应用程序池的各种Recycle选项全部关闭。否则可能因为抓取时间太长,w3wp.exe进程关闭,hang dump抓取失败。
6. 使用WinDbg打开刚才得到的dump文件。执行 .loadby sos.dll mscorwks
7. 执行 !eeheap -gc查看GC的情况,可见有一个heap,因为是单核。如果是双核,则会有两个heap。Heap大小为700多兆,可见是heap出问题了。
8. 执行 !dumpheap –stat:
可见System.String占据了大部分的内存空间。
9. 执行 !dumpheap –type Link并终止输出。找到其中一个object的地址02eab13c,执行 !do 02eab13c:可以发现Stringbuilder和System.String都是包含在Link类型中的类型。
这就解释了!dumpheap –stat中看到的占据heap最多的对象的类型的内存占用情况。
10. 执行 !dumpheap –type System.String 并强行终止输出。以其中一个对象的地址 027da238为例,执行 !do 027da238,可以找到这个string对象存储的值。
11. 为了找到为什么该对象没有被回收,查看哪些线程对它还有root,因此执行命令 !gcroot 027da238:
可见Finalizer queue对其有引用,因此可以推断Finalizer queue还没有对这个对象执行Finalize方法。
12. 执行 !threads找到Finalize queue的线程是 17:
13. 执行 ~17s切换到该线程。执行 !clrstack:
可见这就是Finalize queue上对该对象的root一直保持着的原因:线程休眠了。
14. 在App_Code文件夹里找到哦啊Link.cs:
可见该对象规定了Finalize方法,因此会被放到Finalize queue上去。而这个方法里面,导致执行该方法的Finalize线程休眠了,所以对象一直有引用,内存得不到释放。
20110218添加:
一、本例被判定为High Memory的问题。这样的问题有三种情况,一是Manged Memory泄漏,二是Native Memory泄漏,第三是Load Library出问题。对于这三种情况,可以通过Performance Monitor来进行基本判定。其中.NET CLR Memory object的#bytes in all heaps 这个counter,Process Object的Virutual Bytes和Private bytes这两个counter。
1. 如果是Managed Memory泄漏,那么bytes in all heaps,private bytes和virtual bytes会同市上升。
2. 如果是Native Memory泄漏,那么bytes in all heaps不会上升,但是private bytes和virtual bytes都会上升。
3. 如果是Load Library的问题,那么bytes in all heaps和private bytes都不会上升,但是virutal bytes会上升。
二、本例debug时首先执行了!address -summary 命令,得到如下结果:
0:000> !address -summary -------------------- Usage SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Pct(Busy) Usage 373b7000 ( 904924) : 21.58% 85.85% : RegionUsageIsVAD bfa89000 ( 3140132) : 74.87% 00.00% : RegionUsageFree 76e6000 ( 121752) : 02.90% 11.55% : RegionUsageImage 67c000 ( 6640) : 00.16% 00.63% : RegionUsageStack 0 ( 0) : 00.00% 00.00% : RegionUsageTeb 144a000 ( 20776) : 00.50% 01.97% : RegionUsageHeap 0 ( 0) : 00.00% 00.00% : RegionUsagePageHeap 1000 ( 4) : 00.00% 00.00% : RegionUsagePeb 1000 ( 4) : 00.00% 00.00% : RegionUsageProcessParametrs 2000 ( 8) : 00.00% 00.00% : RegionUsageEnvironmentBlock Tot: ffff0000 (4194240 KB) Busy: 40567000 (1054108 KB) -------------------- Type SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Usage bfa89000 ( 3140132) : 74.87% :834e000 ( 134456) : 03.21% : MEM_IMAGE 95a000 ( 9576) : 00.23% : MEM_MAPPED 378bf000 ( 910076) : 21.70% : MEM_PRIVATE -------------------- State SUMMARY -------------------------- TotSize ( KB) Pct(Tots) Usage 34bea000 ( 864168) : 20.60% : MEM_COMMIT bfa89000 ( 3140132) : 74.87% : MEM_FREE b97d000 ( 189940) : 04.53% : MEM_RESERVE Largest free region: Base 80010000 - Size 7fefa000 (2096104 KB)
从这里面可以得到的信息由:
1. RegionUsageIsVAD占有最多的内存份额,它与GCHeap相关。
2. Pct(Tots)指的是该部分memory占总共的virtual memory的比例,而Pct(Busy)指的是该部分memory占Private memory的比例。
三、由于已经能够判定是Managed Memory有问题,我们需要进一步查看是Managed Memory的哪一步有问题,因此执行!eeheap -gc 对GC占有的memory进行enumerate:
0:000> !eeheap -gc Number of GC Heaps: 2 ------------------------------ Heap 0 (001aa148) generation 0 starts at 0x32f0639c generation 1 starts at 0x32ae3754 generation 2 starts at 0x02eb0038 ephemeral segment allocation context: none segment begin allocated size 001bfe10 7a733370 7a754b98 0x00021828(137256) 001b0f10 790d8620 790f7d8c 0x0001f76c(128876) 02eb0000 02eb0038 06eacb28 0x03ffcaf0(67095280) 100b0000 100b0038 140a4a08 0x03ff49d0(67062224) 18180000 18180038 1c15c650 0x03fdc618(66962968) 20310000 20310038 242eb99c 0x03fdb964(66959716) 28310000 28310038 2c2f04cc 0x03fe0494(66978964) 31190000 31190038 33185ff4 0x01ff5fbc(33513404) Large object heap starts at 0x0aeb0038 segment begin allocated size 0aeb0000 0aeb0038 0aec0b28 0x00010af0(68336) Heap Size 0x15fd1310(368907024) ------------------------------ Heap 1 (001ab108) generation 0 starts at 0x36e665bc generation 1 starts at 0x36a28044 generation 2 starts at 0x06eb0038 ephemeral segment allocation context: none segment begin allocated size 06eb0000 06eb0038 0aea58d4 0x03ff589c(67066012) 14180000 14180038 1817eda8 0x03ffed70(67104112) 1c310000 1c310038 202f0550 0x03fe0518(66979096) 24310000 24310038 28304ca8 0x03ff4c70(67062896) 2c310000 2c310038 2fd62a48 0x03a52a10(61155856) 35190000 35190038 372253f4 0x020953bc(34165692) Large object heap starts at 0x0ceb0038 segment begin allocated size 0ceb0000 0ceb0038 0ceb0048 0x00000010(16) Heap Size 0x15ab1570(363533680) ------------------------------ GC Heap Size 0x2ba82880(732440704)
四、执行!dumpheap -stat查看heap中各种类型占用的内存的大小。这里原文重点解释了Size的意义,用!dumpheap -stat 得到的类型的size值得话并不真实,因为有的对象只是包含了一个别的对象的引用而已,因而计算大小的话只会计算指针大小。需要使用!objsize才能计算出真正的大小。
五、由于对象没有被回收,是因为他们的引用没有得到释放。因此执行!gcroot <address of an object>查看可以的对象的引用情况。
六、在得知对象没有被引用的原因是对象正在被Finalize的过程中时,可以执行!FinalizeQueue查看这个queue引用了哪些对象。这里原文强调了为什么对象会被FinalizeQueue引用:只有默认具有析构函数的系统类型或者显式定义了析构函数的类型的对象,才会在Garbage Collect的时候,被Finalize Queue引用执行其Destructor的方法。