使用WinDbg —— .NET篇 (六)
来源:互联网 发布:二端口纯电阻 编辑:程序博客网 时间:2024/05/21 22:21
6.4 根
根(root)的概念原本打算放在前面讲代的概念的时候提一下,但是根的概念也涉及到终结队列和GC句柄表,所以不适合放在代里面讲;但是讲代的时候难免会涉及到根的相关概念,所以也不适合放在后面,这是个先有鸡还是先有蛋的问题,让人纠结,所以折中一下,在前面个讲代的时候简单的提了一下根的概念,主要把根的概念放在后面讲。
根其实就是指根对象,前面讲的概念里面也大多涉及到对象可不可达的问题。如果对象可达,就说明有至少一个根对象直接引用或间接引用了这个对象。这个根就是最原始的对象,根主要出现在几个地方:
1. 栈里面的局部变量和参数
2. 静态变量
3. 终结列表
4. GC句柄表
在这里对第一点进行补充说明:当执行GC的时候,会挂起所有所有线程,这个时候所有线程中的栈里面的局部变量和参数就是第一点提到的。需要注意的是,在执行GC的时候栈中的某个栈帧上的对象在调用当前的过程之后没有在被引用到的话,也有可能会被回收掉,所以有时候你会看到某个栈帧上的某些变量或者参数的值都找不到了。
在SOS中可以使用命令“!gcroot”检测某个对象的根对象,先来看段代码:
using System;
namespaceTestRoot
{
classA
{
string _str;
public A(stringstr)
{
_str = str;
}
publicvoid Print()
{
Console.Write(_str);
}
}
classB
{
privateeventActionDoPrintCommand;
public B(Action executor)
{
DoPrintCommand += executor;
}
publicvoid Execute()
{
if (DoPrintCommand != null)
{
DoPrintCommand();
}
}
}
classProgram
{
staticBb;
staticvoid Main(string[]args)
{
GetExecutor();
GC.Collect();
b.Execute();
Console.Read();
}
staticvoidGetExecutor()
{
Aa = newA("Execute a print command!");
b = newB(a.Print);
}
}
}
编译执行,然后用Windbg attach进程,并加载SOS扩展命令,然后通过“!dumpheap”命令找到类型A的对象:
0:000> !dumpheap -type TestRoot.A
Address MT Size
02d92f8c 01184e48 12
Statistics:
MT Count TotalSize Class Name
01184e48 1 12 TestRoot.A
Total 1 objects
然后通过“!gcroot”命令找到对象A的根:
0:000> !gcroot 02d92f8c
HandleTable:
00fd13ec (pinned handle)
-> 03d934c8 System.Object[]
-> 02d92fb8 TestRoot.B
-> 02d92f98 System.Action
-> 02d92f8c TestRoot.A
Found 1 unique roots (run '!GCRoot -all' to see all roots).
这里表明了A对象的根是在句柄表中,而且还是被pinned的数组对象,然后这个被pinned的数组对象引用了B对象,B对象通过委托引用了A对象。可以通过“!gchandles”命令进行验证根对象确实在GC句柄表中:
0:000> !gchandles
Handle Type Object Size Data Type
00fd12fc WeakShort 02d95344 52 System.Threading.Thread
00fd11cc Strong 02d94454 48 System.Object[]
00fd11d4 Strong 02d91ba4 36 System.Security.PermissionSet
00fd11d8 Strong 02d91238 28 System.SharedStatics
00fd11dc Strong 02d911c8 84 System.Threading.ThreadAbortException
00fd11e0 Strong 02d91174 84 System.Threading.ThreadAbortException
00fd11e4 Strong 02d91120 84 System.ExecutionEngineException
00fd11e8 Strong 02d910cc 84 System.StackOverflowException
00fd11ec Strong 02d91078 84 System.OutOfMemoryException
00fd11f0 Strong 02d91024 84 System.Exception
00fd11f8 Strong 02d95344 52 System.Threading.Thread
00fd11fc Strong 02d91358 112 System.AppDomain
00fd13ec Pinned 03d934c8 8172 System.Object[]
00fd13f0 Pinned 03d924b8 4092 System.Object[]
00fd13f4 Pinned 03d92298 524 System.Object[]
00fd13f8 Pinned 02d9121c 12 System.Object
00fd13fc Pinned 03d91020 4708 System.Object[]
Statistics:
MT Count TotalSize Class Name
726b299c 1 12 System.Object
726b2a38 1 28 System.SharedStatics
726b33fc 1 36 System.Security.PermissionSet
726b2920 1 84 System.ExecutionEngineException
726b28dc 1 84 System.StackOverflowException
726b2898 1 84 System.OutOfMemoryException
726b2744 1 84 System.Exception
726b31ec 2 104 System.Threading.Thread
726b2ab4 1 112 System.AppDomain
726b2964 2 168 System.Threading.ThreadAbortException
726b29f0 5 17544 System.Object[]
Total 17 objects
Handles:
Strong Handles: 11
Pinned Handles: 5
Weak Short Handles: 1
介绍完了GC的重要概念,可以开始好好说一说怎么分析OOM的问题了,一般涉及到OOM的问题不外乎以下几点原因:
1. 设计原因
2. 大对象
3. 非托管资源
4. 根对象引用
设计原因:
设计原因主要指程序的设计方面的缺陷,例如,有个需求是在某个页面或者Form上显示所有的用户某个类型的数据,其中这个数据量是百万级别的,显示的每个item大概有5~10个字段。针对这么个需求,如果把显示的每个字段按平均10个字节计算,每个item需要显示5个字段,一百万的数据量,那么这个数据总量也有50M。对于这么个页面如果采用一次性加载所有的数据计的话,先不论加载的速度和单条数据的更新等导致的用户体验问题,单单就一个页面或者Form就要用掉至少50M的内存,这个也容易导致极大的内存问题,如果某个显示的item还需要显示图片的话,基本上这个页面或者Form就挂了。
一般来说往往一个程序消耗的内存在高峰时期和在低峰时期相差非常大时,基本上这个设计是可以改进的,当然也有特殊的需求导致程序的设计很难改动,这要具体问题具体分析。像上面说的那个问题很容易解决,就是考虑分页机制。
大对象:
使用大对象有三个缺点:一是大对象的分配需求的内存比较大,而且请求的内存还是连续的,导致申请比较困难;二是大对象不再使用之后被回收的频率比较小,因为大对象的回收是被当做第2代的对象处理的;三是大对象被释放后不会压缩内存,导致有内存碎片的产生(在FrameWork 4.5.1之后可以配置参数进行压缩大对象了)。
一般的大对象都是IO流,其中有文件流或者网络IO流等,为了解决大对象导致的问题,可以考虑牺牲部分性能,将大对象流分成多个小对象流进行处理,这样分成的小对象流避免了大对象的三个缺点,但这样的设计是平衡了部分性能,这个性能消耗主要在压缩内存的时候,移动的数据量比较大,但这个移动的数据量也有限,毕竟GC给GC堆申请的段(Segment)有个固定大小,所以移动的数据量远小于这个固定大小。在这里引用一位大神写的替代MemoryStream类的小对象流的设计,供参考:
http://www.codeproject.com/Articles/348590/A-replacement-for-MemoryStream
非托管资源:
非托管资源的忘记释放也是比较常见的一个问题,一般我们用到的非托管资源有Image、Font、文件对象、Socket等。对于这类的非托管对象的检测一般用的非SOS扩展命令,一般的命令有“!address-summary”和“!heap -s”:
或者:
0:000> !heap -s
NtGlobalFlag enables following debugging aids for new heaps:
tail checking
free checking
validate parameters
LFH Key : 0xa9a2727d
Termination on corruption : ENABLED
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
01000000 40000062 1020 480 1020 16 58 1 0 1
011d0000 40001062 60 12 60 1 2 1 0 0
00ff0000 40001062 60 12 60 0 2 1 0 0
02c60000 40001062 60 4 60 0 1 1 0 0
02d80000 40041062 60 4 60 2 1 1 0 0
-----------------------------------------------------------------------------
有个很好的分析范例:
http://kate-butenko.blogspot.com/2012/07/investigating-issues-with-unmanaged.html
根对象引用:
根对象引用导致的问题是最常见的问题,一般都是因为长生命的对象依赖了短生命的对象,从而延长了短命对象的生命。例如,不断的往静态的集合字段中添加数据项,其中某个数据项的生命较短,但这个静态集合的生命跟Appdomain一样,只有当AppDomain被卸载的时候才会回收静态字段。所以当这个数据项不在使用而且也不从这个静态集合中移除掉的时候,该数据项的生命就被延长了。一般来说,写代码最容易犯也最容易忽视的根引用是委托事件,往往在注册事件或者初始化委托实例的时候都不会手动写代码去取消注册或者移除委托实例,这就导致了连接的两个对象的生命保持了对等,我碰到过的最多的关于OOM问题就是委托事件导致的内存泄露问题。在我演示GC句柄表的时候使用的代码就是用了委托,其中延长了A对象的生命,所以在后面执行GC的时候,A对象没有被回收掉。对于此类问题一般都是通过SOS中的扩展命令“!gcroot”检测对象为什么没有被回收掉。
总的来说分析OOM的问题没有一个固定的套路,这需要依靠开发人员对内存机制的了解,对理论了解的越深,才能举重若轻的对OOM问题进行细致可靠的分析。在这里题外话,说一个个人经历,我有次在分析OOM的时候,分析了很久也没发现有问题,然后把dump文件扔给一个朋友,然后他分析过后就很明了的告诉我结论是什么,这点让我很惊讶。我事后看了他的分析过程,使用的命令都很平常,但他就能从一些不起眼的内存片段中找到问题所在,这就是内力深厚的表现。
最后总结一下个人分析OOM经验或者说是一个基本的套路:在分析OOM的时候,如果有OOM Exception,或许分析过程会变得简单,因为这个时候可以很明显的看到哪个对象严重超标,而且还可以利用SOS中的“!AnalyzeOOM”命令或者相应的简写“!ao”;如果是分析还没有崩溃的程序的Memory Leak,那么一个很好的思路是分析两个时间段打出来的dump文件,对比其中的对象列表,分析出具体对象的异常从而抽丝拨茧找到Memory Leak的地方。如果是在做某个特定的操作的时候,内存持续走高,虚拟内存换页频繁,操作做完之后内存恢复正常,没有Memory Leak的问题。针对这种情况也需要警惕,因为这样的程序很可能在其他内存相对较少的机器上会发生OOM的问题。这个时候也需要分析内存高峰时期的Dump,查看分析哪个对象的生命被延长了,导致内存持续走高。
- 使用WinDbg —— .NET篇 (六)
- 使用WinDbg —— .NET篇 (一)
- 使用WinDbg —— .NET篇 (二)
- 使用WinDbg —— .NET篇 (三)
- 使用WinDbg —— .NET篇 (四)
- 使用WinDbg —— .NET篇 (五)
- 使用WinDbg —— .NET篇 (七)
- 使用WinDbg —— .NET篇 (八)
- 使用WinDbg —— .NET篇 (九)
- 使用WinDbg —— .NET篇 (十)
- 使用WinDbg —— .NET篇 (十一)
- 使用WinDbg —— .NET篇 (十二)
- 使用WinDbg —— .NET篇 (十三)
- 使用Windbg 调试.Net程序
- 使用Windbg调试.Net应用程序
- 使用Windbg 调试.Net程序
- 快速入门:使用WINDBG调试.NET 程序
- WinDbg学习笔记六
- ZOj 3471 Most Powerful 状态压缩DP
- hdu1251 统计难题(字典树)
- java基础
- 深入理解C++中的mutable关键字
- Android 切换jdk on Mac OS
- 使用WinDbg —— .NET篇 (六)
- iOS多线程编程之NSThread的使用
- scrapy 爬网站 显示 Filtered offsite request to 错误.
- 安卓开发底层,应用,测试必看
- Linux IPC实践 --System V共享内存
- java基础2
- 3、继承、初始化⽅法、便利构造器
- android面试题笔试题总结
- Fragment实现的底部导航