要确认以上的发现,并学到更多的内容,知道并掌握asp.net 进程中发生了什么,我们可以用 ADPlus 这个脚本文件来创建dump 文件,然后用WinDbg 和SOS扩展来检查这个dump文件。
有关使用 ADPlus ,请查看:
在提示符中输入 iisreset 来重启IIS
缺省情况下,当内存使用超过60%,asp.net 进程就会回收。为了方便调试我们可以修改一些默认的行为,让调试的时间更加的充裕。
增加 注册表键值:
你需要添加 DebugOnHighMem和UnderDebugger ,这些是告诉asp.net 调用DebugBreak 方法来,而不是结束这个进程。
方法:
1) 打开注册表,找到:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET
2) 在ASP.NET上点击右键,新建,选择“DWORD值”。
3) 把默认新建的“新值 #1”改成 DebugOnHighMem。
4) 双击该新建的键,把十六进制的值从0 改成1。
5) 创建另外一个 DWORD 值,命名为“UnderDebugger”值是 0。
这个是.net 1.1的修改方式,在.net 2.0 中这个方式有所变化,可以直接在配置文件中修改。具体请google查询。当一个调试器被附加到asp.net 进程,内存限制机制被自动禁用。UnderDebugger DWORD 的值设置为0,则该机制没有被禁用。
配置ADPlus文件来创建dump文件
ADPlus脚本可以创建一个完整的dump文件,当运行在 –crash模式下,按 CTRL+C来中断。打开ADPlus.vbs 把Full_Dump_on_CONTRL_C 从 FALSE 改成 TRUE.
' Set Full_Dump_on_CONTRL_C to TRUE if you wish to have AD+ produce a full
' memory dump (as opposed to a mini-memory dump) whenever CTRL-C is pressed to stop
' debugging, when running in 'crash' mode.
' Note: Depending upon the number of processes being monitored, this could
' generate a significant amount of memory dumps.
Const Full_Dump_on_CONTRL_C = TRUE
即使进程没有崩溃(crash),你也可以使用 –crash 模式来收集进程数据
方法:
打开命令窗口运行:
c:\debuggers>adplus.vbs -pn aspnet_wp.exe -crash -quiet -o
c:\labs\highMemory
-quiet 选项可以剔除额外的进程信息。-o 指定声称dump文件的路径,运行过程中你还会看到一个最小化的 CDB.exe 进程窗口。
注意dump文件可能是会很大的。每次点击 “Allocate 20 MB Objects” 这个按钮,100M被申请,如果你的内存是512M,点击按钮3次,你需要300M 或更多的磁盘空间。
OK, 现在运行页面,分配3次 内存,每次20M,点击“Allocate 20 MB Objects”3次,然后点击“Refresh Stats”。
调试器终止了进程,产生了日志和dump文件,如果你不指定输出路径,则有如下的一个文件生成:
c:\debuggers\Crash_Mode__Date_05-19-2002__Time_19-41-21PM
PID-344__ASPNET_WP.EXE__CTRL-C__full_2002-05-19_19-43-59-798_0158.dmp 这是一个典型的文件名,full 表示是full dump
打开WinDbg,从“File”菜单中点击“Open Crash Dump”如果询问“save base workspace information”,点击“No”,如果有一个分割(Disassembly)窗口弹出,关闭它,然后从“window”菜单中点击 “Automatically Open Disassembly”。
调式符号(Symbol)也是需要的,而且要和当前操作系统相符合,不符合是绝对不行的。
按照下面的方式输入符号
从命令行输入:通过环境变量 _NT_SYMBOL_PATH 来为WinDbg设定符号路径。
.sympath SRV*c:\symbols\debugginglabs*http://msdl.microsoft.com/download/symbols;C:\symbols\debugginglabs;C:\Program Files\Microsoft Visual Studio.NET\FrameworkSDK\symbols;C:\windows\system32
在WinDbg菜单上点击“file”,选择“Symbol File Path”,然后输入:
SRV*c:\symbols\debugginglabs*http://msdl.microsoft.com/download/symbols;C:\symbols\debugginglabs;C:\Program Files\Microsoft Visual Studio.NET\FrameworkSDK\symbols;C:\windows\system32
“SRV”告诉 WinDbg去找符号服务器,然后拷贝符号文件到本地。
记得要保存这个 WorkSpace,否则下次使用 还要再次输入这个路径。调试托管代码,你还需要SOS.dll 扩展。请看其他的文章来配置这个。
来检查托管代码:
1) 打开 WinDbg 命令窗口,(Alt+1)
2) 输入: .load d:\windows\....\sos.dll (这里输入你的sos所在的路径)
3) 使用 !findtable 命令来初始化SOS(现在可以不用输入这个命令了)
下面开始分析dump文件,这个步骤不是凭简单的直接来的。
在进入细节之前,这里有些总体的思路来处理dump文件。
当分析内存问题时,考虑GC的行为特点。你可以使用 !eeheap 来列出托管堆的大小,然后和dump文件的大小比较一下,接下来 找到是什么托管对象占用了托管堆。在这个例子中 使用 !dumpheap 来发现 有多少 System.Byte [] 对象在大对象堆上。
你需要知道这些对象会存在多久,它们有多大。 这个 !gcroot 命令可以显示 在 System.Byte [] 对象上由强引用,那意味着它们被根化的(rooted )。System.Web.Caching.Cache 类引用了 System.Byte [] 对象,所以它才被根化了。
现在知道了被引用的对象,那就尝试去找出它们占用了多少内存吧。
找出 System.Web.Caching.Cache 的内存地址,使用“ !name2ee 程序集(dll) 类名 ”的方法,这个命令返回一个 MethodTable 的地址,然后你可以使用 !dumpheap 来dump出有那些MethodTable的对象的内容。
这个 !dumpheap 命令给出了 实际的 System.Web.Caching.Cache 对象的地址,那些对象可以使用 !objsize 命令找出在GC 堆上这个被根化(rooted)的活的对象的大小。这些对象的大小是可以比得上dump文件的大小的。你可以推断出大部分的托管内存是被System.Web.Caching.Cache 对象引用了。
下面看看具体的分析:
首先看看GC heap的大小,和dump 文件比较一下。
这个 !eeheap 命令列出了GC 堆的大小 和G0,G1,G2 ,LOH的开始地址。
0:001> !eeheap –gc
generation 0 starts at 0x0110be64
generation 1 starts at 0x01109cd8
generation 2 starts at 0x01021028
segment begin allocated size
01020000 01021028 0110de70 000ece48(970312)
Total Size 0xece48(970312)
------------------------------
large block 0x11e1fc04(300022788)
large_np_objects start at 17b90008
large_p_objects start at 02020008
------------------------------
GC Heap Size 0x11f0ca4c(300993100)
在这里GC 堆是300M左右,dump文件是358M。 使用 !dumpheap –stat 命令来查看占用 了空间的托管对象。这个命令输出的结果包含几个列:MT ,Count ,TotalSize,Class Name。这个MT 输出代表 Method Table,MT的意思是代表对象的类型,这些MT是按照 Total size 的大小升序排列的。Total Size 代表对象的大小和对象个数的乘积。
因为有一定量的数据要处理和显示,!dumpheap –stat 命令可以看作是处理时间上的一个序列。下面是一组输出:
0:001> !dumpheap -stat
Bad MethodTable for Obj at 0110d2a4
Last good object: 0110d280
total 14459 objects
Statistics:
MT Count TotalSize Class Name
3c6185c 1 12 System.Web.UI.ValidatorCollection
3c2e110 1 12 System.Web.Configuration.MachineKeyConfigHandler
3c29778 1 12 System.Web.Configuration.HttpCapabilitiesSectionHandler
3c23240 1 12 System.Web.SafeStringResource
3c22484 1 12 System.Web.Configuration.HandlerFactoryCache
3c22028 1 12 System.Web.UI.PageHandlerFactory
3c21a48 1 12 System.Web.Configuration.HttpHandlersSectionHandler
3b5f730 1 12 System.Web.Configuration.AuthorizationConfigHandler
3b5f54c 1 12 System.Web.Configuration.AuthorizationConfig
3b5e458 1 12 System.Web.Configuration.AuthenticationConfigHandler
3b5ccb8 1 12 System.Web.SessionState.InProcStateClientManager
3b5c528 1 12 System.Web.SessionState.SessionStateSectionHandler
3b5c1c4 1 12 System.Web.SessionState.SessionOnEndTarget
3b5c108 1 12 System.Web.Caching.OutputCacheItemRemoved
3b5c078 1 12 System.Web.Security.FileAuthorizationModule
3b5bfa8 1 12 System.Web.Security.UrlAuthorizationModule
下面的图显示了 该命令的后面几行,有最大 TotalSize的MT类型的对象是System.String, System.Byte [], 和 System.Object []。
D12f28 1133 46052 System.Object[]
153cb0 88 76216 Free
321b278 85 178972 System.Byte[]
d141b0 6612 416720 System.String
Total 14459 objects
System.String 是最大的对象,这个 Free 表示那些已经被GC回收,没有被引用的,但那些内存还没有被释放到操作系统的实体。
后面还有一些的显示是大对象堆(LOH)上的对象
large objects
Address MT Size
17b90018 321b278 20000012 System.Byte[]
16220018 321b278 20000012 System.Byte[]
14f00018 321b278 20000012 System.Byte[]
13650018 321b278 20000012 System.Byte[]
12330018 321b278 20000012 System.Byte[]
11010018 321b278 20000012 System.Byte[]
ec50018 321b278 20000012 System.Byte[]
d560018 321b278 20000012 System.Byte[]
bed0018 321b278 20000012 System.Byte[]
a8a0018 321b278 20000012 System.Byte[]
92d0018 321b278 20000012 System.Byte[]
7d60018 321b278 20000012 System.Byte[]
6850018 321b278 20000012 System.Byte[]
53a0018 321b278 20000012 System.Byte[]
3f50018 321b278 20000012 System.Byte[]
2020018 d12f28 2064 System.Object[]
2020840 d12f28 4096 System.Object[]
可以使用 !gcroot 可以从LOH上得到更多对象的信息,例如:!gcroot 17b90018 提供了前面列表有关的第一个 System.Byte [] 对象的信息,
下面是 输出:
0:001> !gcroot 17b90018
Scan Thread 1 (4e8)
Scan Thread 5 (bb0)
Scan Thread 6 (d0)
Scan Thread 10 (43c)
Scan Thread 11 (308)
Scan Thread 12 (6e4)
Scan HandleTable 14e340
Scan HandleTable 150e40
Scan HandleTable 1a6fa8
HANDLE(Strong):37411d8:Root:020784d8(System.Object[])-
>0108b504(System.Web.HttpRuntime)->0108b9d0(System.Web.Caching.CacheSingle)-
>0108ca68(System.Web.Caching.CacheUsage)->0108ca78(System.Object[])-
>0108cb3c(System.Web.Caching.UsageBucket)-
>010f95fc(System.Web.Caching.UsageEntry[])-
>01109c8c (System.Web.Caching.CacheEntry)->00000000()
那些 “Scan Thread” 的行表示 !gcroot 命令正在扫描被根引用了对象的线程,那些“Scan HandleTable”的行表示这个命令也正在扫描roots 集合的 HandleTables。每个应用程序域(AppDomain)是一个独立的应用程序执行环境,它有一个 HandleTable,HandleTable 就是一个简单的另外资源的引用。引用可以在寄存器、参数或局部变量中找到。
如果输出包含:HANDLE(Strong),表示 一个强引用找到了,意味着这个对象被根化(rooted)并且不能被回收。还有其它的引用也找得到了,下面列出了。
这些dump显示的是和 System.Byte [] 相同的数据,从 dumpheap –stat 的输出结果中再找一个地址,这个输出和前面一个是相同的,除了高亮的 System.Web.Caching.CacheEntry的地址
0:001> !gcroot 16220018
Scan Thread 1 (4e8)
Scan Thread 5 (bb0)
Scan Thread 6 (d0)
Scan Thread 10 (43c)
Scan Thread 11 (308)
Scan Thread 12 (6e4)
Scan HandleTable 14e340
Scan HandleTable 150e40
Scan HandleTable 1a6fa8
HANDLE(Strong):37411d8:Root:020784d8(System.Object[])-
>0108b504(System.Web.HttpRuntime)->0108b9d0(System.Web.Caching.CacheSingle)-
>0108ca68(System.Web.Caching.CacheUsage)->0108ca78(System.Object[])-
>0108cb3c(System.Web.Caching.UsageBucket)-
>010f95fc(System.Web.Caching.UsageEntry[])-
>01109be8 (System.Web.Caching.CacheEntry)->00000000()
找出cache使用了多少内存将是非常有用的。要找出System.Web.Caching.Cache的地址,请使用 !name2ee命令,这个命令接受2个参数 程序集的名字和全类名。如果你不知道 System.Web.Caching.Cache的程序集,使用帮助来查找。你通过System.Web.Caching.CacheSingle 也可以得到相同的数据。
0:001> !name2ee System.Web.dll System.Web.Caching.Cache
--------------------------------------
MethodTable: 03887998
EEClass: 03768814
Name: System.Web.Caching.Cache
--------------------------------------
EEClass 是一个用来表示.net 类的内部结构。可以通过SOS帮助找到更多的信息。
要知道一个托管堆中的某个对象的类型,使用 !dumpheap –mt MethodTable地址 的方式来获得。
0:001> !dumpheap -mt 03887998
Address MT Size
0108b8ac 03887998 12
Bad MethodTable for Obj at 0110d2a4
Last good object: 0110d280
total 1 objects
Statistics:
MT Count TotalSize Class Name
3887998 1 12 System.Web.Caching.Cache
Total 1 objects
large objects
Address MT Size
total 0 large objects
使用 –mt 选项可能是比较难以理解的因为你不需要MethodTable 的有关信息。然而,当一个地址传给 !dumpheap –mt ,所有运行中的在这个域中的这个类型的对象都会被列出来。如果有多个域,每个实例的对象都会列出来。
你可以把地址传给 !objsize 命令来查看对象的大小。上面列出的结果中只有一个地址,要查看 System.Web.Caching.Cache 的大小,使用 !objsize 0108b8ac ,下面显示的就是你希望看到的结果:
0:001> !objsize 0108b8ac
sizeof(0108b8ac) = 300126128 (0x11e38fb0) bytes (System.Web.Caching.Cache)
注意: !objsize 命令不带参数,则是列出在进城中所有的根(roots)集合中的对象。
释放内存
当你点击“Free Memory”,你可以从性能监视器中看到 private bytes 的最大值还是在222M。并没有释放。
这是清除缓存的代码:
ArrayList arr = (ArrayList)Session["CacheList"];
foreach (string s in arr)
{
if (Cache[s] != null)
{
Cache.Remove(s);
Label1.Text += "<TABLE BORDER>";
Label1.Text += "<TR><TD>";
Label1.Text += "Emptying out cachekey " + s;
Label1.Text += "</TD></TR></TABLE>";
}
}
arr.Clear();
Session["CacheList"] = arr;
代码中也包含 GC.Collect() ,这段代码清除缓存,引发垃圾收集,但并没有导致分配给大对象堆(LOH)的虚拟内存释放。下图显示了cache被清除,GC.TotalMemory 的下降,但是 Private Bytes 仍然很高。
当“Free Memory”被点击后,除了 1M内存外其它都被释放了。 这个 # Bytes in all Heaps 计数器有明显的下降,但是Private Bytes 因为LOH的关系没有改变。下图显示了:
捕获一个dump文件
为了获得更多的进程状态信息,以 –hang 模式来运行 ADPlus 来捕获一个进程状态dump文件。
adplus.vbs–hang–pn aspnet_wp.exe–quiet
启动WinDbg,装在 dump文件,加载模块,使用 !eeheap –gc 来看看托管堆的大小:
0:000> !eeheap -gc
generation 0 starts at 0x012cc0e4
generation 1 starts at 0x012afde8
generation 2 starts at 0x011c1028
segment begin allocated size
011c0000 011c1028 012d6000 00114fd8(1134552)
Total Size 0x114fd8(1134552)
------------------------------
large block 0x8060(32864)
large_np_objects start at 00000000
large_p_objects start at 021c0008
------------------------------
GC Heap Size 0x11d038(1167416)
dump 显示 GC 堆的大小是1M,不是222M,这表示除了1M其它的都被收集了。那些分配了20M的对象,你可以得出一个结论:他们都被移除了。但 Provate Bytes仍然是222M,那你需要进一步研究。
在调试器中,运行 !dumpheap –stat ,下面是输出:
...
d19dd4 79 23608 System.Char[]
32169e8 382 26304 System.Int32[]
32174a0 179 39048 System.Collections.Hashtable/bucket[]
d12f28 1136 46352 System.Object[]
153bb0 91 146208 Free
321b278 90 179136 System.Byte[]
d141b0 6626 472128 System.String
Total 14525 objects
large objects
Address MT Size
21c0018 e82f28 2064 System.Object[]
21c0840 e82f28 4096 System.Object[]
21c1858 e82f28 4096 System.Object[]
2217cb0 e82f28 2064 System.Object[]
22184d8 e82f28 4096 System.Object[]
22194f0 e82f28 4096 System.Object[]
221a508 e82f28 2064 System.Object[]
221ad30 e82f28 4096 System.Object[]
221bd48 e82f28 2064 System.Object[]
226e930 e82f28 2064 System.Object[]
226f158 e82f28 2064 System.Object[]
total 11 large objects
总体来说:asp.net 页面分配了20M的大对象,然后cache中缓存,因为这些对象都是大于85K,他们被放到了LOH中,当对象从cache中移除,托管堆(性能监视器中的 # Bytes in all Heaps 计数器)从200M减小到1M。 这个进程的 private bytes 没有释放,仍然是222M。虽然托管堆释放了,但由 VirtualAlloc 函数分配给进程的内存还在。