使用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流等,为了解决大对象导致的问题,可以考虑牺牲部分性能,将大对象流分成多个小对象流进行处理,这样分成的小对象流避免了大对象的三个缺点,但这样的设计是平衡了部分性能,这个性能消耗主要在压缩内存的时候,移动的数据量比较大,但这个移动的数据量也有限,毕竟GCGC堆申请的段(Segment)有个固定大小,所以移动的数据量远小于这个固定大小。在这里引用一位大神写的替代MemoryStream类的小对象流的设计,供参考:

http://www.codeproject.com/Articles/348590/A-replacement-for-MemoryStream

非托管资源:

非托管资源的忘记释放也是比较常见的一个问题,一般我们用到的非托管资源有ImageFont、文件对象、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,查看分析哪个对象的生命被延长了,导致内存持续走高。

 

0 0