.Net 调式案例—实验3 内存(Memory)回顾 System.OutOfMemoryException
.Net 调式案例—实验3 内存(Memory)回顾 System.OutOfMemoryException
今天的调试课程中的主要问题是内存的研究。这次我们将压力测试BuggyBits站点,制造高内存使用量并找出原因。这个实验有点长,因为我要讲述内存问题研究的各个方面。一旦尼知道dump文件的中的一些东西和性能计数器中的一些东西的关联你就可以跳过一些方面。但我仍然建议收集性能日志,这样比较完整。
更多的内容请参看“梅川酷子's Blog 渣科技”,它里面有非常多的有关性能的问题
前面的案例和设置指导
请参看前面发布的文章。
设置性能计数器日志
1) 浏览http://localhost/BuggyBits/Links.aspx来让w3wp.exe 进程启动。
2) 打开一个性能计数器,从开始/运行 中输入:perfmon.exe。
3) 右键在点击“性能日志和警告/日志计数器”上点击“新建日志设置”,在弹出的对话框中输入:Lab3-Mem。
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
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
0 ( 0) : 00.00% 00.00% : RegionUsageTeb
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
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 0x
generation 1 starts at 0x32ae3754
generation 2 starts at 0x02eb0038
ephemeral segment allocation context: none
segment begin allocated size
001bfe10
001b
…..
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 0x
generation 2 starts at 0x06eb0038
ephemeral segment allocation context: none
segment begin allocated size
06eb0000 06eb0038 0aea58d4 0x03ff
……
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
7912d
7912d9bc 820 273384 System.Collections.Hashtable+bucket[]
6639e
0fe11cf4 36000 576000 Link
790fdc
790fd
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\f
Fields:
MT Field Offset
790fdc
790fd
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 790fd
------------------------------
Heap 0
...
5)把它们中的一些输出来看看里面是什么
!do <address of string> ,地址是 !dumpheap -mt 输出的第一列。
0:000> !do
...
String: http://www.sula.cn
...
Q:这些string里面包含的是什么?
A:好像link.aspx 页面显示了link对象。
6)拣几个,看看它们被根化(rooted)到哪里(即为什么它们不会被回收)。注意你可能需要尝试不同的几个才行。
!gcroot <address of string>
0:000> !gcroot
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
Scan Thread 14 OSTHread 1120
Scan Thread 24 OSTHread
Finalizer queue:Root:
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 (
generation 1 has 0 finalizable objects (
generation 2 has 45 finalizable objects (
Ready for finalization 18009 objects (
------------------------------
Heap 1
generation 0 has 338 finalizable objects (
generation 1 has 4 finalizable objects (
generation 2 has 36 finalizable objects (
Ready for finalization 17707 objects (
Statistics:
MT Count TotalSize Class Name
79116758 1 20 Microsoft.Win32.SafeHandles.SafeTokenHandle
79103764 1 20 Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
7910b630 2 40 System.Security.Cryptography.SafeProvHandle
79112728 5 100 Microsoft.Win32.SafeHandles.SafeWaitHandle
790fe704 2 112 System.Threading.Thread
...
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
...
0:020> !clrstack
OS Thread Id: 0x1b94 (20)
ESP EIP
Q:什么对象正在被终结?
A:看起来是一个link 对象。
Q:它正在干什么? 为什么这个会导致高内存使用率?
A:终结link对象的终结器线程因为sleep 被堵住了。意味着终结器被堵住,进程中没有东西可以被终结。因而等待终结的进程都会仍然在内存中直到终结器醒来它们被终结为止。
查看代码,确认上面的分析
打开Link.cs 的代码,看看析构/终结(destructor/finalizer)的有疑问的代码。
该文来自:http://www.sula.cn/97.shtml ,转载请注明出处。