.Net 调式案例—实验3 内存(Memory)回顾 System.OutOfMemoryException web
今天的调试课程中的主要问题是内存的研究。此次咱们将压力测试BuggyBits站点,制造高内存使用量并找出缘由。这个实验有点长,由于我要讲述内存问题研究的各个方面。一旦尼知道dump文件的中的一些东西和性能计数器中的一些东西的关联你就能够跳过一些方面。但我仍然建议收集性能日志,这样比较完整。 数据库
前面的案例和设置指导 windows
请参看前面发布的文章。 服务器
设置性能计数器日志 session
1) 浏览http://localhost/BuggyBits/Links.aspx来让w3wp.exe 进程启动。 app
2) 打开一个性能计数器,从开始/运行 中输入:perfmon.exe。 asp.net
3) 右键在点击“性能日志和警告/日志计数器”上点击“新建日志设置”,在弹出的对话框中输入:Lab3-Mem。 dom
4) 在接下来弹出的对话框中点击“添加对象…”。 函数
5) 添加对象“.NET CLR Memory”和“Process”,点击关闭。 工具
6) 把时间间隔设置成1秒,由于咱们这个例子运行的时间比较短。
7) 修改运行方式,把“<默认>”修改为“domain\username”(把默认这个东西删除掉,把你机器的用户名输入进去),而后点击设置密码,把你的用户名对应的密码输入进去。这样作的目的是你要以管理员(administrator)或具备对w3wp.exe 调试权限的用户来运行日志计数器。不然将得不到.net 计数器的值。
8) 点击确认,这个将会启动性能监视器对进程的监控。
问题再现
1) 在命令行中,切换到tinyget的目录。
2) 运行:tinyget -srv:localhost -uri:/BuggyBits/Links.aspx -loop:4000,注意,若是你发现你的进程由于OutOfMemory异常崩溃了,你能够稍微下降循环的次数,我这里设置的这么高是想获得好一点的效果。
3) 中止性能计数器。(在运行tinyget后至少要让他再运行20到30秒)
检查性能计数器的日志
1) 在性能监视器中,选择“系统监视器”这个节点。
2) 点击工具栏上“查看日志数据”(像数据库同样的那个图标),而后打开日志文件,请尽量多的囊括更多的时间。
3) 把窗口下面那些默认的计数器都删除掉。
4) 添加一些计数器:
·.NET CLR Memory/# Bytes in all Heaps
·.NET CLR Memory/Large Object Heap Size
·.NET CLR Memory/Gen 2 heap size
·.NET CLR Memory/Gen 1 heap size
·.NET CLR Memory/Gen 0 heap size
·Process/Private Bytes
·Process/Virtual Bytes
这些计数器将会在窗口底部显示出来。
5) 在图表任意位置上点击右键,选择“属性”,在“数据”的选项卡上在比例一栏中选择0,0000001,确保你能够在一个屏幕上看到在整个图表。若是你点击“突出显示”那个工具栏的按钮,那被选中的计数器就会高亮显示,更有利于知道那个计数器的含义。
Q:这些计数器的最新值是什么?
A:
Q:比较一下 Virtual Bytes, Private Bytes 和 #Bytes in all Heaps,他们的曲线是否是一致的增加或降低,仍是分歧很大?
A:
这三个曲线 #Bytes in all heaps (底部), Private bytes (中间) 和virtual bytes (顶部)是一致的增加的,这意味着内存的增加是因为#Bytes in all heaps (.net GC memory)的增加而增加的。
要查找出是什么类型的“内存泄露”,咱们可使用以下的规则:
a) virtual bytes 增加,Private bytes保持平稳 => 是virtual bytes 泄漏,例如一些组件保留了一些内存,但没有使用它。=> 使用调试工具来跟踪下去看看。
b) Private bytes 增加,#Bytes in all heaps 保持平稳 => 本地(原生)或加载器堆(loader heap)泄漏 => 使用调试工具来跟踪查看,另外看看程序集(assemblies)的数量是否是增长了(看 .net clr loading 下的计数器)。
c) #Bytes in all heaps 和 private bytes 彼此一块儿增加或降低 => 研究 dotnet GC 堆。
在virtual bytes和 private bytes之间有200-300M的差异,在服务器进程中这是彻底正常的,特别是在一个dotnet进程中,由于有不少内存被保留用来初始化GC 堆(其余比如dll等这些事情),那些 virtual bytes 的增加也是块状的,那也是正常的,由于堆(内存)典型的是以块申请的。
Q:从这些数据你可否告诉咱们这是本地(原生)内存泄露,仍是.net 内存泄露,或者是在装载堆(动态程序集)上的内存泄露?
A:经过上面的观察,这是一个.net 内存泄露。
Q:在日志记录的结尾(当tinyget运行结束后),内存保持平稳仍是降低?
A:它仍是保持平稳,就是说在压力测试后内存没有被回收。
6) 添加
.NET CLR Memory/# Gen 0 Collections,
.NET CLR Memory/# Gen 1 Collections,
.NET CLR Memory/# Gen 2 Collections
三个计数器。
Q:当tinyget运行完后,收集行为有发生么?若是没有,为何呢?
A:垃圾收集(GC)仅仅发生在分配内存时或你主动调用GC.Collect()时。这个概念是一个重点,请你记住。在咱们的压力测试事后,咱们并无两个条件中的一个,所以没有人会收集那些内存,除非它必需要这么作(其余有需求时,操做系统来作,或dotnet的其余程序)。
7) 打开任务管理器,在“进程”选项卡上,点击菜单上的“查看/选择列”,勾选上“虚拟内存大小”,把“内存使用”和“虚拟内存大小”的值与性能计数器中的“Private Bytes” 和“Virtual Bytes”进行比较,(注意:任务管理器中给出的是以K为单位的,你须要乘以1024)。
Q:“内存使用”显示什么东西?
Q:“虚拟内存大小”显示什么东西?
A:在我这里,这两个指标在任务管理器显示了大约746MB,而后当我抓去dump文件时, 内存使用跳到了大约864MB,在这里的重点是理解这些值得含义。
另外关于性能监视器中的一些解释:
Private Bytes |
"the current size, in bytes, of memory that this process has allocated that cannot be shared with other processes." |
Virtual Bytes |
"the current size, in bytes, of the virtual address space the process is using. Use of virtual address space does not necessarily imply corresponding use of either disk or main memory pages. Virtual space is finite, and the process can limit its ability to load libraries." |
Working Set |
"the current size, in bytes, of the Working Set of this process. The Working Set is the set of memory pages touched recently by the threads in the process. If free memory in the computer is above a threshold, pages are left in the Working Set of a process even if they are not in use. When free memory falls below a threshold, pages are trimmed from Working Sets. If they are needed they will then be soft-faulted back into the Working Set before leaving main memory." |
#Bytes in all heaps |
"This counter is the sum of four other counters; Gen 0 Heap Size; Gen 1 Heap Size; Gen 2 Heap Size and the Large Object Heap Size. This counter indicates the current memory allocated in bytes on the GC Heaps." |
Gen 0 heap size |
"This counter displays the maximum bytes that can be allocated in generation 0 (Gen 0); its does not indicate the current number of bytes allocated in Gen 0. A Gen 0 GC is triggered when the allocations since the last GC exceed this size. The Gen 0 size is tuned by the Garbage Collector and can change during the execution of the application. At the end of a Gen 0 collection the size of the Gen 0 heap is infact 0 bytes; this counter displays the size (in bytes) of allocations that would trigger the next Gen 0 GC. This counter is updated at the end of a GC; its not updated on every allocation." |
Gen 1 heap size |
"This counter displays the current number of bytes in generation 1 (Gen 1); this counter does not display the maximum size of Gen 1. Objects are not directly allocated in this generation; they are promoted from previous Gen 0 GCs. This counter is updated at the end of a GC; its not updated on every allocation." |
Gen 2 heap size |
"This counter displays the current number of bytes in generation 2 (Gen 2). Objects are not directly allocated in this generation; they are promoted from Gen 1 during previous Gen 1 GCs. This counter is updated at the end of a GC; its not updated on every allocation." |
LOH size |
"This counter displays the current size of the Large Object Heap in bytes. Objects greater than |
这里有一些注意的事情:
1) Gen 0 heap size 显示的是Gen 0 的预算大小,不是在Gen 0 中的全部对象的大小。
2) Gen 0,1,2 和LOH计数器是在每次收集后才更新的,不是在每次分配后。
3) 对于LOH的解释,20K是针对1.0 版本的,在1.1 和2.0 后大对象的大小是大于85000字节的。
4) 这个进程的工做集环境是由一些RAM中的页面文件组成,这些页面文件是由该进程建立的,这个进程的private bytes 是没有什么事情能够作的(咱们在查看任务管理器的时候,咱们经常看内存使用量,它并不能表明什么),进城使用的内存量可能多于或少于private bytes。对于winform的应用程序(没有服务进程的)有很大的差异,当你最小化一个winform程序时,程序会对内存进程对齐操做。而对于服务程序,例如asp.net 不会这样,private bytes 和工做集会保持在一样的范围里面,不会改变。
获得一个dump文件
1) 在命令提示符下面,切换到调试器目录,运行:“adplus -hang -pn w3wp.exe -quiet”。
在WinDbg中打开dump文件
1) 在WinDbg中打开dump文件。
2) 设置符号文件路径和加载sos。
Q:dump文件多少大?在windows资源管理器中看看就知道了。
A: 864 096 k
Q:这个文件的大小和Private Bytes, Virtual Bytes 和 # Bytes in all Heaps 比较一下,看看怎么样?
A:看起来,它们都是差很少大的。
检查内存,想一想内存去哪里了
1) 运行 !address –summary (这个命令给你一个内存使用的概要),让你本身熟悉一下这些输出。能够查看WinDbg 的帮助看看 !address。
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)
这里有很是多的信息,但全部的这些都不是这么明显的。相信我,让咱们花一些时间在这里。在任何一个案例中,我用这个来帮助我指出我须要查看哪些地方,因此我不介意它要花费多少,即便就是一个概述的结果。
一些要注意的地方:
上面一屏显示了按照类型不一样而分类显示的由进程使用的内存。第一部分是按照区域类型来划分的,它按照什么样子的分配类型告诉你信息。最常遇到的一个类型是VAD = Virtual Alloc, Image = dlls 和 exes,Heap = heaps the process owns,从WinDbg的帮助中能够获得更多的信息。接着下面是按IMAGE, MAPPED 或 PRIVATE 的类型来列出,最后一部分是按已提交(also committed,就是指实际已经分配的)或保留(reserved)的方式来列出它们。
Tot: ffff0000 (4 194 240 kb) :的意思是我总共有4GB的虚拟内存地址空间提供给这个应用程序。32位系统上,你能够寻地4GB的空间,典型的是2GB的用户模式的内存空间,因此通常你会看到2GB而不是这里的4GB,在64位上,运行一个32位的进程会获得彻底的4GB的空间,因此我这里看到的是4GB。
Busy: 40567000 (1 054 108 kb) 是咱们已经使用的(已经分配的)。
MEM_PRIVATE是一个私有的内存,它不和其余进程共享内存,不是映射到文件的内存。不要把这个和性能计数器中的Private Bytes混淆。这里的MEM_PRIVATE 是保留+已提交(即已分配的)(reserved + committed)的字节数,另外那个Private Bytes 是申请/已提交(allocated/committed)的字节数。
MEM_PRIVATE 是已经提交(已经分配)的内存(不必定是 private的),这个多是最接近你获得的Private Bytes的。
MEM_RESERVE 是已经保留的,但没有实际分配的,未提交的内存。全部已经分配的内存也是定义为保留的,因此若是你查看全部保留的内存(最接近你获得的virtual bytes),你必须加上MEM_COMMIT和 MEM_RESERVE,它是显示在Busy 中的那个数字。你本身把数字加上后比对一下看看。
Q:哪一个值最能表明以下两个指标?
·Private Bytes A: MEM_COMMIT
·Virtual Bytes A: Busy
Q:大部分的内存都去哪里了?(哪一个区域)
A:在这里,大约904MB是为VAD保留的,VAD是dotnet对象存放的地方,由于GC堆是virtual allocs 分配的。
Q:Busy,Pct(Busy),Pct(Tots)是什么意思?
A:Pct(Tots) 显示的是整个虚拟地址空间中分配给不一样区域类型的百分比。Pct(Busy)显示的是保留的内存中分配给不一样区域的百分比。Pct(busy) 很显然是我最关心的一个。
Q:MEM_IMAGE 是什么意思?
A:从帮助文件中咱们知道:这个是表示从一个可执行的映射文件的一部分映射到的内存。换句话说 就是dll 或一个exe 文件的内存映射。
Q:哪一个区域的.net 内存是适宜的,为何?
A:在RegionUsageIsVAD,理由如上。
从性能计数器中咱们看到#Bytes in all Heaps 跟随着Private bytes的增加而增加,那说明了内存的增长几乎都是.net 的使用而增长的,进而咱们转化为为何.net 的GC堆(heap)始终在增加。
检查.net GC 堆(heap)
1) 运行 !eeheap –gc 来查看.net GC 堆的大小
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)
…..
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)
……
Large object heap starts at 0x0ceb0038
segment begin allocated size
0ceb0000 0ceb0038 0ceb0048 0x00000010(16)
Heap Size 0x15ab1570(363533680)
------------------------------
GC Heap Size 0x2ba82880(732440704)
Q:总共有多少个Heap,为何?
A:这里有两个堆,由于咱们运行在多核进程模型中。
Q:有多少内存被保存在了.net GC 堆中?拿#Bytes in all Heaps比较一下。
A:GC的堆大小是:GC Heap Size 0x2ba82880(732 440 704),它和性能计数器中的bytes in all heaps很接近。
Q:large object heap 上有多少内存?提示:把large object heap段上的合计加起来,和性能计数器中的Large Object Heap Size 比较一下。
A:它是很是小的,因此LOH看起来不是问题所在,大小是68 336 + 16 bytes
2) 运行 !dumpheap –stat 来输出全部的以统计式样表示的.net 对象。
Q:查看 5 到10个使用了大部份内存的对象,思考一下是什么泄露了?
A:
66424cf4 37 57276 System.Web.Caching.ExpiresEntry[]
663b0cdc 4001 192048 System.Web.SessionState.InProcSessionState
7912d8f8 3784 255028 System.Object[]
7912d9bc 820 273384 System.Collections.Hashtable+bucket[]
6639e4c0 4037 290664 System.Web.Caching.CacheEntry
0fe11cf4 36000 576000 Link
790fdc5c 36161 723220 System.Text.StringBuilder
001a90c0 1105 7413924 Free
790fd8c4 51311 721773112 System.String
Total 163943 objects
大部分的内存是被strings用掉了,这个不太正常,虽然strings 在应用中是最多见的,可是大约721MB的显然有点怪异,而且有3600个Links(不管它们是什么),看起来有点奇怪。特别是由于有差很少数量的stringbuilds 出如今dump中。
Q:“size”那个行显示了什么?例如,“size”这行包含了什么?
A:若是咱们用命令!do 把Link 对象输出来,咱们看到有一个指针指向stringbuilder(url)和一个指针指向string(name),link对象的大小是16B,这个大小仅仅包含了指针的大小和其余一些开销(methos table 等)。
若是你运行 !objsize ,你会看见大小是高达20144 B ,这个大小是包含成员变量的,好比Link对象的大小和它引用的全部对象。
你看到了什么经过!dumpheap 输出的16B的每个link。它不包含成员变量的大小是由一些不一样缘由的:
1)它将要花费很长的时间去计算大小。
2) 一些对象(假如是A和B)可能都指向C对象,若是你使用 !objsize 计算A 和B的大小,他们都会包含C的大小,因此size这个列的值会变得很复杂难以计算。
3) 在这个例子中Link的大小size看起来彷佛是正常的。由于一个link对象包含一个url和一个name。可是若是一个web 控件可能会包含一个成员变量 _parent ,若是你运运行 !objsize ,这样就会包含父对象(page)那就显然是不合适的。
0:000> !do 371d44cc
Name: Link
MethodTable: 0fe11cf4
EEClass: 0fde5824
Size: 16(0x10) bytes
(C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\
Temporary ASP.NET Files\buggybits\b27906cc\f5f91899\App_Code.wbwztx_4.dll)
Fields:
MT Field Offset Type VT Attr Value Name
790fdc5c 4000006 4 ...ext.StringBuilder 0 instance 371d44dc url
790fd8c4 4000007 8 System.String 0 instance 02f13cd8 name
0:000> !objsize 371d44cc
sizeof(371d44cc) = 20144 ( 0x4eb0) bytes (Link)
一般,我不推荐马上查看在你的这个很是简单的dump文件中,在该命令输出的底部的strings,由于:
· strings 这一行的“size”是实际的字符串string的有内容的真实大小。若是你和DataSet比较,这个“size”只是包含了行和列的指针,并无包含行和列的内存。因此DataSet这个对象的大小几乎老是很是的小的。
· string 字符串在大部分的对象中几乎是叶子节点,例如,dataset包含字符串,aspx页面包含字符串,session 变量也包含字符串。因此,在一个应用中几乎都是字符串。
然而在这个例子中,字符串有这么多,占有了那么多的内存。若是咱们不查到其余一些阻止了咱们的东西,那咱们可能就要沿着string 这条路走下去了。
3) 把各类不一样大小的string 都输出来,找出哪些string是愈来愈大的。(里面可能有一些讨厌的事和错误,因此你要尝试不一样的大小来找出那些是愈来愈大的)
获得string的 MT(method table),!dumpheap –stat 的输出结果的第一列。
!dumpheap -mt <string MT> -min 85000 -stat
!dumpheap -mt <string MT> -min 10000 -stat
!dumpheap -mt <string MT> -min 20000 -stat
!dumpheap -mt <string MT> -min 30000 -stat
!dumpheap -mt <string MT> -min 25000 -stat
Q:大部分的string’在一个什么样的范围内?
A:在 20000 和 25000 字节之间。
4)把那个范围内的string 输出来。
!dumpheap -mt <string MT> -min 20000 -max 25000
在这里,它们中的大部分是如出一辙的大小的,这是一个指引咱们向下前进的线索。
0:000> !dumpheap -mt 790fd8c4 -min 20000 -max 25000
------------------------------
Heap 0
Address MT Size
02f1412c 790fd8c4 20020
02f2d96c 790fd8c4 20020
02f327c4 790fd8c4 20020
02f3761c 790fd8c4 20020
02f3c474 790fd8c4 20020
02f412cc 790fd8c4 20020
02f46124 790fd8c4 20020
02f4af7c 790fd8c4 20020
02f4fdd4 790fd8c4 20020
02f54c2c 790fd8c4 20020
...
5)把它们中的一些输出来看看里面是什么
!do <address of string> ,地址是 !dumpheap -mt 输出的第一列。
0:000> !do 02f327c4
...
String: http://www.sula.cn
...
Q:这些string里面包含的是什么?
A:好像link.aspx 页面显示了link对象。
6)拣几个,看看它们被根化(rooted)到哪里(即为何它们不会被回收)。注意你可能须要尝试不一样的几个才行。
!gcroot <address of string>
0:000> !gcroot 02f327c4
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 16 OSTHread 1948
Scan Thread 20 OSTHread 1b94
Scan Thread 21 OSTHread 1924
Scan Thread 22 OSTHread 188c
Scan Thread 14 OSTHread 1120
Scan Thread 24 OSTHread 13f8
Finalizer queue:Root:02f327a0(Link)->
02f327b0(System.Text.StringBuilder)->
02f327c4(System.String)
Q:它们被根化到哪里?为何?
A:这个string是一个string builder 类型的成员变量,它表现的是一个link的成员变量(url),link 对象被根化在终结器队列中,那就是说他正在等待被终结。
检查终结器队列(finalizer queue)和终结线程(finalizer thread)
1)查看终结器队列
!finalizequeue
0:000> !finalizequeue
SyncBlocks to be cleaned up: 0
MTA Interfaces to be released: 0
STA Interfaces to be released: 0
----------------------------------
------------------------------
Heap 0
generation 0 has 221 finalizable objects (0f44a764->0f44aad8)
generation 1 has 0 finalizable objects (0f44a764->0f44a764)
generation 2 has 45 finalizable objects (0f44a6b0->0f44a764)
Ready for finalization 18009 objects (0f44aad8->0f45c43c)
------------------------------
Heap 1
generation 0 has 338 finalizable objects (0f45d840->0f45dd88)
generation 1 has 4 finalizable objects (0f45d830->0f45d840)
generation 2 has 36 finalizable objects (0f45d7a0->0f45d830)
Ready for finalization 17707 objects (0f45dd88->0f46f234)
Statistics:
MT Count TotalSize Class Name
663a1fc8 1 12 System.Web.Configuration.ImpersonateTokenRef
79116758 1 20 Microsoft.Win32.SafeHandles.SafeTokenHandle
791037c0 1 20 Microsoft.Win32.SafeHandles.SafeFileMappingHandle
79103764 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
6639c104 1 20 System.Web.PerfInstanceDataHandle
663f6b5c 1 28 System.Web.Security.FileSecurityDescriptorWrapper
663a105c 1 32 System.Web.Compilation.CompilationMutex
7910b630 2 40 System.Security.Cryptography.SafeProvHandle
79112728 5 100 Microsoft.Win32.SafeHandles.SafeWaitHandle
790fe704 2 112 System.Threading.Thread
7910a5c4 2 120 System.Runtime.Remoting.Contexts
...
Q:在这个命令的输出中列出了什么对象?
A:全部具备终结/析构器的都被注册到终结器队列中,当对象被垃圾收集时,终结器会运行析构函数,不然在dispose函数中终结过程会挂起。
Q:有多少个对象是出于“ready for finalization”,它是什么意思?
A:大约有36000个,这些对象是要被垃圾收集的,正在等待被终结。若是ready for finalization大于0 但没有显示任何信息,这是一个说明终结器线程被堵塞的最好时机。因此这些对象被堵住了等待终结,他们消耗了大部分的内存。
2) 找出终结线程,了解它正在干什么,运行!threads ,在列出的线程中查找带有“(Finalizer)”的线程。
3) 切换到终结线程,检查托管的和本地(原生)的调用堆栈。
~5s (把5 替换成真实的终结线程(finalizer thread)的ID号)
kb 2000
!clrstack
0:000> !threads
...
20 2 1b94 001ac2c0 200b220 Enabled 00000000:00000000 001ccc80 0 MTA (Finalizer)
...
0:020> !clrstack
OS Thread Id: 0x1b94 (20)
ESP EIP
02a0f8fc 7d61cca8 [HelperMethodFrame: 02a0f8fc] System.Threading.Thread.SleepInternal(Int32)
02a0f950 0fe90ce8 Link.Finalize()
02a0fc1c 79fbcca7 [ContextTransitionFrame: 02a0fc1c]
02a0fcec 79fbcca7 [GCFrame: 02a0fcec]
Q:什么对象正在被终结?
A:看起来是一个link 对象。
Q:它正在干什么? 为何这个会致使高内存使用率?
A:终结link对象的终结器线程由于sleep 被堵住了。意味着终结器被堵住,进程中没有东西能够被终结。于是等待终结的进程都会仍然在内存中直到终结器醒来它们被终结为止。
查看代码,确认上面的分析
打开Link.cs 的代码,看看析构/终结(destructor/finalizer)的有疑问的代码。