SOS扩展系列---利用SOS计算变量生存期
下面这个程序有一个bug,试试看你能不能抓住它。
Test.cs (编译成 DelegateExample.exe):
usingSystem;usingSystem.Threading;usingSystem.Runtime.InteropServices;classTest
{delegate uintThreadProc (IntPtr arg);private uintm;public Test (uintn)
{
m=n;
}uintReflect (IntPtr arg)
{
Console.WriteLine (m);returnm;
}static voidMain ()
{
Test t= new Test (1);
ThreadProc tp= newThreadProc (t.Reflect);
NewThread (tp);
Thread.Sleep (1000);
}
[DllImport("UsingCallback")]static extern voidNewThread (ThreadProc proc);
}
WinDbg常用命令系列---!runaway
简介
!runaway扩展显示有关每个线程使用的时间的信息。
使用形式
!runaway [Flags]
参数
- Flags
指定要显示的信息类型。 标志可以是以下位的任意组合。 默认值为 0x1。
位 0 (0x1)
使调试器以显示每个线程使用的用户时间量。
位 1 (0x2)
使调试器以显示每个线程使用的内核时间量。位 2 (0x4)
使调试器以显示每个线程创建以来已经过去的时间量。
支持环境
Windows 2000 |
Uext.dll Ntsdexts.dll |
Windows XP 及更高版本 |
Uext.dll Ntsdexts.dll |
说明
这个扩展是一种快速的方法,可以找出哪些线程正在失控地旋转或占用太多的CPU时间。显示标识每个线程由调试器的内部线程编号和线程 ID 以十六进制格式。 调试器 Id 也会显示。
下面是一个例子:
0:001> !runaway 7
User Mode Time
Thread Time
0:55c 0:00:00.0093
1:1a4 0:00:00.0000
Kernel Mode Time
Thread Time
0:55c 0:00:00.0140
1:1a4 0:00:00.0000
Elapsed Time
Thread Time
0:55c 0:00:43.0533
1:1a4 0:00:25.0876
遍历GC堆
了解对象在gc堆中的布局是很有用的。在垃圾收集期间,有效的对象是通过递归访问对象来标记的,这些对象从堆栈的根和句柄开始。但是,从每个堆段的开始到结束,对象的位置以一种有组织的方式排列也很重要。!DumpHeap命令依赖于这个逻辑组织来正确地遍历堆,如果它报告了一个错误,可以打赌您的堆出了问题。
下面是代码
usingSystem;usingSystem.Reflection;usingSystem.Reflection.Emit;usingSystem.Threading;usingSystem.Runtime.InteropServices;public classEmitHelloWorld
{
[DllImport("kernel32")]public static extern voidDebugBreak();static void Main(string[] args)
{//create a dynamic assembly and module AssemblyName assemblyName = newAssemblyName();
assemblyName.Name= "HelloWorld";
AssemblyBuilder assemblyBuilder=Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder module;
module= assemblyBuilder.DefineDynamicModule("HelloWorld.exe");//create a new type to hold our Main method TypeBuilder typeBuilder = module.DefineType("HelloWorldType", TypeAttributes.Public |TypeAttributes.Class);//create the Main(string[] args) method MethodBuilder methodbuilder = typeBuilder.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Static | MethodAttributes.Public, typeof(void), new Type[] { typeof(string[]) });//generate the IL for the Main method ILGenerator ilGenerator =methodbuilder.GetILGenerator();
ilGenerator.EmitWriteLine("hello, world");
ilGenerator.Emit(OpCodes.Ret);//bake it Type helloWorldType =typeBuilder.CreateType();//run it helloWorldType.GetMethod("Main").Invoke(null, new string[] {null});
DebugBreak();//set the entry point for the application and save it assemblyBuilder.SetEntryPoint(methodbuilder, PEFileKinds.ConsoleApplication);
assemblyBuilder.Save("HelloWorld.exe");
}
}
WinDbg常用命令系列---!findstack
简介
!findstack扩展查找所有包含指定的符号或模块的堆栈。此命令搜索线程调用堆栈中的特定符号,并显示匹配的线程。
使用形式
!findstackSymbol[DisplayLevel]
!findstack -?
参数
- Symbol
指定符号或模块。
- DisplayLevel
指定显示内容。这可以是以下任何一个值。默认值为1
0
显示仅包含每个线程的线程 ID符号。1
显示线程 ID 和包含每个线程的帧符号。2
将显示包含每个线程的整个线程堆栈符号。
- -?
在调试器命令窗口中显示此扩展的一些简要帮助文本。
说明
内核模式此扩展还显示有关堆栈,包括每个线程的状态的简短摘要信息。
此扩展的输出的一些示例如下:
0:023> !uext.findstack wininet
Thread 009, 2 frame(s) match
* 06 03eaffac 771d9263 wininet!ICAsyncThread::SelectThread+0x22a
* 07 03eaffb4 7c80b50b wininet!ICAsyncThread::SelectThreadWrapper+0xd
Thread 011, 2 frame(s) match
* 04 03f6ffb0 771cda1d wininet!AUTO_PROXY_DLLS::DoThreadProcessing+0xa1
* 05 03f6ffb4 7c80b50b wininet!AutoProxyThreadFunc+0xb
Thread 020, 6 frame(s) match
* 18 090dfde8 771db73a wininet!CheckForNoNetOverride+0x9c
* 19 090dfe18 771c5e4d wininet!InternetAutodialIfNotLocalHost+0x220
* 20 090dfe8c 771c5d6a wininet!ParseUrlForHttp_Fsm+0x135
* 21 090dfe98 771bcb2c wininet!CFsm_ParseUrlForHttp::RunSM+0x2b
* 22 090dfeb0 771d734a wininet!CFsm::Run+0x39
* 23 090dfee0 77f6ad84 wininet!CFsm::RunWorkItem+0x79
Thread 023, 9 frame(s) match
* 16 0bd4fe00 771bd256 wininet!ICSocket::Connect_Start+0x17e
* 17 0bd4fe0c 771bcb2c wininet!CFsm_SocketConnect::RunSM+0x42
* 18 0bd4fe24 771bcada wininet!CFsm::Run+0x39
* 19 0bd4fe3c 771bd22b wininet!DoFsm+0x25
* 20 0bd4fe4c 771bd706 wininet!ICSocket::Connect+0x32
* 21 0bd4fe8c 771bd4cb wininet!HTTP_REQUEST_HANDLE_OBJECT::OpenConnection_Fsm+0x391
* 22 0bd4fe98 771bcb2c wininet!CFsm_OpenConnection::RunSM+0x33
* 23 0bd4feb0 771d734a wininet!CFsm::Run+0x39
* 24 0bd4fee0 77f6ad84 wininet!CFsm::RunWorkItem+0x79
0:023> !uext.findstack wininet!CFsm::Run 0
Thread 020, 2 frame(s) match
Thread 023, 3 frame(s) match
0:023> !uext.findstack wininet!CFsm 0
Thread 020, 3 frame(s) match
Thread 023, 5 frame(s) match
OutOfMemoryException和对象锁定
众所周知,在CLR中,内存管理是由垃圾收集器(GC)完成的。当GC在新对象的预分配内存块(GC堆)中找不到内存,并且无法从操作系统预订足够的内存来扩展GC堆时,它抛出OutOfMemoryException(OOM)。
问题
我不时听到关于OOM的抱怨——人们分析代码并监控内存使用情况,发现有时当有足够的可用内存时,他们的.NET应用程序会抛出OOM。在我见过的大多数情况下,问题是:
- 操作系统的虚拟地址空间是碎片。这通常是由应用程序中的某些非托管组件引起的。这个问题在非托管世界中存在很长一段时间,但它可能会对GC造成严重影响。GC堆以段为单位进行管理,在V1.0和V1.1中,工作站版本的GC堆大小为16MB,服务器版本为32MB。这意味着当CLR需要扩展GC堆时,它必须为服务器应用程序找到32MB连续的可用虚拟内存。在用户模式下,对于2GB地址空间的系统,这通常不是问题。但是,如果应用程序中有一些非托管dll不小心操作虚拟内存,则虚拟地址空间可能会被划分为可用内存和保留内存的小块。因此,GC将无法找到足够大的空闲内存,尽管总的空闲内存足够。这种问题可以通过查看整个虚拟地址空间来找出哪个块被哪个组件保留。
- GC堆本身是碎片化的,这意味着GC不能在已经保留的段中分配对象,而这些段实际上有足够的空闲空间。我想在这个博客里关注这个问题。
GC堆一瞥
通常托管堆不应该出现碎片问题,因为在GC期间堆是压缩的。下面显示了一个过于简单化的CLR GC堆模型:
- 所有对象彼此相邻;堆的顶部是可用空间。
|---------|
|free |
|_________|
|Object B |
| |
|_________|
|Object A |
|_________|
| ... |
- 新对象在空闲空间中分配。分配总是发生在顶部,就像堆栈一样。
|---------|
|free |
|_________|
|Object C |
|_________|
|Object B |
| |
|_________|
|Object A |
|_________|
| ... |
- 当空闲空间用完时,会发生GC。在GC期间,标记可访问的对象。
|---------|
|Object C | (marked)
|_________|
|Object B |
| |
|_________|
|Object A | (marked)
|_________|
| ... |
- 在GC之后,堆被压缩,活动(可访问)对象被重新定位,死(不可访问)对象被清除。
|---------|
|free |
|_________|
|Object C |
|_________|
|Object A |
|_________|
| ... |
GC堆中的可用空间
在上面的模型中,您可以看到GC实际上在对堆进行碎片整理方面做得很好。可用空间始终位于堆的顶部,可用于新的分配。但在实际生产中,空闲空间可以驻留在分配的对象之间。这是因为:
- 有时,GC可以选择在不必要时不压缩堆的一部分。由于重新定位所有对象的成本可能很高,因此在某些情况下,GC可能会避免这样做。在这种情况下,GC将在堆中保留一个可用空间列表,以备将来压缩。这不会导致堆碎片化,因为GC完全控制可用空间。GC可以在以后任何时候在必要时填充这些块。
- 固定对象不能移动。因此,如果一个固定对象在GC中幸存下来,它可能会创建一个可用空间块,如下所示:
GC前:GC后:
|---------| |---------|
|Object C | (pinned, reachable) |Object C | (pinned)
|_________| |_________|
|Object B | (unreachable) | free |
| | | |
|_________| |_________|
|Object A | (reachable) |Object A |
|_________| |_________|
| ... | | ... |
如何锁定分割GC堆
如果应用程序一直以这种模式固定对象:固定一个新对象,执行一些分配,固定另一个对象,执行一些分配。。。并且所有固定的对象长时间保持固定,会产生大量的自由空间,如下图所示:
- 锁定新对象
|---------|
|free |
|_________|
|Pinned 1 |
|_________|
|Object A |
|_________|
| ... |
- 分配后,另一个对象被锁定
|---------|
|free |
|_________|
|Pinned 2 |
|_________|
| ... |
|_________|
|Pinned 1 |
|_________|
|Object A |
|_________|
| ... |
- 更多的对象被锁定,中间有未锁定的对象
|_________|
|Pinned n |
|_________|
| ... |
|_________|
|Pinned 2 |
|_________|
| ... |
|_________|
|Pinned 1 |
|_________|
|Object A |
|_________|
| ... |
- GC发生了,因为无法重新定位固定的对象,所以堆中仍有可用空间
|_________|
|Pinned n |
|_________|
| free |
|_________|
|Pinned 2 |
|_________|
| free |
|_________|
|Pinned 1 |
|_________|
| free |
|_________|
| ... |
这样的进程可以创建一个具有大量空闲插槽的GC堆。这些空闲槽被部分重用用于分配,但是当它们太小或者剩余的空间太小时,只要对象被固定住,GC就不能使用它们。这将阻止GC有效地使用堆,并可能最终导致OOM。
有一点使情况更糟的是,尽管开发人员可能不直接使用固定对象,但有些.Net库在幕后使用它们,比如异步IO。例如在V1.0和V1.0中传递到缓冲区接受器Socket被库固定,以便非托管代码可以访问缓冲区。考虑一个socket服务器应用程序,它每秒处理数千个socket请求,由于连接速度慢,每个请求可能需要几分钟的时间;GC堆可能会因大量的固定对象和较长的生存期而碎片化,有些对象被钉住;那么OOM就可能发生。
如何诊断问题
要确定GC堆是否碎片化,SOS是最好的工具。Sos.dll是.NET framework附带的调试器扩展,它可以检查CLR中的某些基础数据结构。例如,“DumpHeap”可以遍历GC堆并转储堆中的每个对象,如下所示:
0:000>!dumpheap
Address MT Size
00a71000 0015cde812Free
00a7100c 0015cde812Free
00a71018 0015cde812Free
00a71024 5ba583286800a71068 5ba583806800a710ac 5ba584306800a710f0 5ba5dba468...
00a91000 5ba88bd8206400a91810 0019fe482032Free
00a92000 5ba88bd8409600a93000 0019fe488192Free
00a95000 5ba88bd84096...
total1892objects
Statistics:
MT Count TotalSize Class Name
5ba7607c1 12System.Security.Permissions.HostProtectionResource
5ba75d541 12System.Security.Permissions.SecurityPermissionFlag
5ba61f181 12System.Collections.CaseInsensitiveComparer
...
0015cde86 10260Free
5ba57bf8318 18136System.String
...