Windbg检查托管代码的内存泄露

本文介绍了一个测试程序如何暴露出托管代码中的内存泄露问题,并详细展示了使用Windbg进行内存泄露检查的过程,包括定位泄露源和解决方法。

在写托管代码的过程中,有一些地方很容易造成程序的内存持续增长,直到程序结束时才能释放,下面以一个测试程序为例子讲述怎么检查托管代码的内存泄露:

 

1. 运行测试程序TestCLRMemoryLeak.exe,运行Windbg,并Attach该程序。此时程序的Heap大小为1640372,继续运行程序一段时间

 

0:007> .loadby sos mscorwks
0:007> !eeheap
Loader Heap:

... ...
GC Heap Size  0x1907b4(1640372)
0:007> g

 

2. 此时程序的Heap内存已经变的大很多哦

0:008> !eeheap
Loader Heap:

... ...
GC Heap Size  0x11e9188(18780552)

3. 因此需要检查有什么对象没有被释放

 

0:008> !dumpheap -stat
total 181593 objects
Statistics:
      MT          Count    TotalSize    Class Name

01040054      579       148224     TestCLRMemoryLeak.Page1

... ...

 

4. 哇,TestCLRMemoryLeak.Page1, 这个类的对象都是临时对象,怎么会有这么多在内存中呢?挑其中一个对象分析一下

 

0:008> !dumpheap -mt 01040054     
 Address       MT     Size
01961f68 01040054      256    
0196a800 01040054      256    
... ...

0:008> !gcroot 0196a800
... ...

ESP:30f220:Root:018c26c4(TestCLRMemoryLeak.App)->
018cc740(System.Windows.ResourceDictionary)->
01962cf8(System.Collections.Generic.List`1[[System.Windows.DeferredResourceReference, PresentationFramework]])->
07d5fb70(System.Object[])->
0196ad2c(System.Windows.DeferredAppResourceReference)->
0196ad44(System.EventHandler)->
0196acf0(System.Windows.ResourceReferenceExpression)->
0196a800(TestCLRMemoryLeak.Page1)
... ...
0:008> !do 0196acf0
... ...

Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
6495ea94  4000dfb        4         System.Int32  1 instance        8 _flags
6369061c  4000dfa      1d0        System.Object  0   static 018f71b0 NoValue
6369061c  40026ca        8        System.Object  0 instance 0196acd0 _resourceKey
... ...

0:008> !do 0196acd0
Name: System.String
MethodTable: 63690a00
EEClass: 6344d64c
Size: 32(0x20) bytes
 (D:/WINDOWS/assembly/GAC_32/mscorlib/2.0.0.0__b77a5c561934e089/mscorlib.dll)
String: MyBrush

5. 发现这个问题是由于Page1用了资源MyBrush造成的。从网上搜索可以知道,WPF在使用DynamicResource时有内存泄露。按照网上提供的解决方案可以解决这个问题

     public  partial  class  App :  Application
    {
         protected  override  void OnStartup( StartupEventArgs e)
        {
            WalkDictionary( this.Resources);
 
             base.OnStartup(e);
        }
 
         private  static  void WalkDictionary( ResourceDictionary resources)
        {
             foreach ( DictionaryEntry entry  in resources)
            {
            }
 
             foreach ( ResourceDictionary rd  in resources.MergedDictionaries)
                WalkDictionary(rd);
        }
    }

具体可以看这个地址http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/b97a5f83-5394-430e-9a78-9d3a957e3537/

 

6. 修改后继续检查发现Page1还是没有释放,我们需要再挑其中一个对象分析一下

 

0:008> !gcroot 01a13d38
... ...

ESP:1cec0c:Root:019784c4(System.Windows.Threading.Dispatcher)->
01991dbc(System.Windows.Input.InputManager)->
01992464(System.Windows.Input.StylusLogic)->
01992598(System.Collections.Generic.Dictionary`2[[System.Object, mscorlib],[System.Windows.Input.PenContexts, PresentationCore]])->
019925e4(System.Collections.Generic.Dictionary`2+Entry[[System.Object, mscorlib],[System.Windows.Input.PenContexts, PresentationCore]][])->
019e0508(System.Windows.Interop.HwndSource)->
01980550(TestCLRMemoryLeak.Window1)->
019e0370(System.Windows.EffectiveValueEntry[])->
01a1c778(System.Windows.EventHandlersStore)->
01a1c7a4(MS.Utility.SingleObjectMap)->
01a1c784(MS.Utility.FrugalObjectList`1[[System.Windows.RoutedEventHandlerInfo, PresentationCore]])->
01a4c9c8(MS.Utility.ArrayItemList`1[[System.Windows.RoutedEventHandlerInfo, PresentationCore]])->
01c7e1ac(System.Windows.RoutedEventHandlerInfo[])->
01a1c758(System.Windows.RoutedEventHandler)->
01a13d38(TestCLRMemoryLeak.Page1)
... ...

0:008> !do 01a1c758
Name: System.Windows.RoutedEventHandler
MethodTable: 598b5118
EEClass: 59672958
Size: 32(0x20) bytes
 (D:/WINDOWS/assembly/GAC_32/PresentationCore/3.0.0.0__31bf3856ad364e35/PresentationCore.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
6369061c  40000ff        4        System.Object  0 instance 01a13d38 _target
6368fe74  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
636932c8  4000101        c        System.IntPtr  1 instance   93c270 _methodPtr
636932c8  4000102       10        System.IntPtr  1 instance        0 _methodPtrAux
6369061c  400010c       14        System.Object  0 instance 00000000 _invocationList
636932c8  400010d       18        System.IntPtr  1 instance        0 _invocationCount
0:008> !IP2MD 93c270
Failed to request MethodData, not in JIT code range
0:008> !U 93c270
Unmanaged code
0093c270 e8455b0e64      call    mscorwks!PrecodeFixupThunk (64a21dba)
0093c275 5e              pop     esi
0093c276 0000            add     byte ptr [eax],al
0093c278 fc              cld
0093c279 9b              wait
0093c27a 93              xchg    eax,ebx
0093c27b 0000            add     byte ptr [eax],al
0093c27d 0000            add     byte ptr [eax],al
0093c27f 0000            add     byte ptr [eax],al
0093c281 0000            add     byte ptr [eax],al

 

7. 咦,找不到这个函数,难道没有被加载?

 

0:008> dd 93c270
0093c270  0e5b45e8 00005e64 00939bfc 00000000
0093c280  00000000 00000000 00000000 00000000
0093c290  00000000 00000000 00000000 00000000
0093c2a0  00000000 00000000 00000000 00000000
0093c2b0  00000000 00000000 00000000 00000000
0093c2c0  00000000 00000000 00000000 00000000
0093c2d0  00000000 00000000 00000000 00000000
0093c2e0  00000000 00000000 00000000 00000000
0:008> !dumpmd 00939bfc
Method Name: TestCLRMemoryLeak.Page1.MainWindow_LostFocus(System.Object, System.Windows.RoutedEventArgs)
Class: 00b61904
MethodTable: 01790054
mdToken: 0600002a
Module: 00932c5c
IsJitted: no
CodeAddr: ffffffff

 

8. OK,就是这个函数,程序中加了MainWindow_LostFocus,但是没有相应的地方减去该函数,代码如下

 

    public partial class Page1 : Page
    {
        private int lostFocusCount = 0;
        public Page1()
        {
            InitializeComponent();

            App.Current.MainWindow.LostFocus += new RoutedEventHandler(MainWindow_LostFocus);
        }

        void MainWindow_LostFocus(object sender, RoutedEventArgs e)
        {
            ++lostFocusCount;

            Trace.WriteLine("MainWindow_LostFocus");
        }
    }

 

9. 经检查发现函数MainWindow_LostFocus还没有被JIT编译加载,所以上述第7步需要通过DD命令才能找到对应的函数。该函数被调用后再用命令检查结果如下:

 

0:008> !IP2MD c4c120
Failed to request MethodData, not in JIT code range
0:008> !U c4c120
Unmanaged code
00c4c120 e953460a00      jmp     00cf0778
00c4c125 5f              pop     edi
00c4c126 0000            add     byte ptr [eax],al
00c4c128 98              cwde
00c4c129 8ac4            mov     al,ah
00c4c12b 0000            add     byte ptr [eax],al
00c4c12d 0000            add     byte ptr [eax],al
00c4c12f 0000            add     byte ptr [eax],al
00c4c131 0000            add     byte ptr [eax],al
00c4c133 0000            add     byte ptr [eax],al
0:008> !dumpmd 00cf0778
00cf0778 is not a MethodDesc
0:008> !IP2MD 00cf0778
MethodDesc: 00c48a98
Method Name: TestCLRInlineCode.Page1.MainWindow_CollectionChanged(System.Object, System.EventArgs)
Class: 00d70e38
MethodTable: 00c48b00
mdToken: 06000015
Module: 00c42c5c
IsJitted: yes
CodeAddr: 00cf0778

<think>好的,我需要帮助用户了解如何使用Windbg分析托管和非托管内存泄漏。首先,我得回忆一下之前用户提供的引用内容,这些引用提到了使用WinDbg的各种场景,比如通过gcroot、dumpheap等命令来定位问题,以及区分托管和非托管泄漏的方法。用户现在明确询问分析这两种内存泄漏的方法或教程,所以我要整理出步骤,并且可能需要比较两者的不同处理方式。 首先,分析内存泄漏通常需要确认是托管还是非托管的问题。根据引用[3]和[4],使用WinDbg的!address -summary命令可以查看内存占用情况,如果非托管堆(如Heap)占用高,可能指向非托管泄漏。托管泄漏则可能涉及大量未释放的托管对象,这时候需要用SOS扩展的命令,比如!dumpheap -stat来统计对象。 接下来,针对托管内存泄漏,可能需要检查哪些对象堆积,使用gcroot追踪引用链,找出无法被GC回收的原因。而非托管泄漏可能需要检查托管堆的分配情况,比如用umdh工具对比堆快照,或者使用!heap命令分析堆块。 需要注意的是,引用[2]提到综合使用gcroot、dumpheap和finalizequeue等指令来快速定位问题,这可能对托管泄漏特别有效。而非托管泄漏可能需要不同的方法,比如检查CRT库的调试功能或使用leak命令。 用户可能需要具体的步骤,比如如何加载SOS扩展,如何生成和分析dump文件,以及常用的命令示例。此外,区分托管和非托管的关键点是什么,比如通过内存增长模式,或者使用不同的命令来观察内存分布。 可能还需要提到内存泄漏分析的迭代过程,如引用[1]所述,需要多次尝试和结合应用程序的内部机制。同时,引用[3]和[4]中的案例说明实际分析中的挑战,比如如何判断泄漏来源,可能需要结合业务知识和代码分析。 最后,用户可能需要了解资源释放的责任方,比如非托管资源需要显式释放,而托管资源依赖GC。如果对象持有非托管资源但没有正确实现Dispose模式,可能导致泄漏,这时候finalizequeue命令可能有帮助。 总结起来,回答应该分步骤讲解托管和非托管泄漏的分析方法,结合具体命令和工具,并引用提供的资料作为支持。同时,生成相关问题,帮助用户进一步探索。</think>### Windbg分析托管/非托管内存泄漏方法详解 #### 一、核心分析流程 1. **判断泄漏类型** 通过`!address -summary`查看内存分布: - 非托管泄漏:`Heap`占用异常增长且无合理释放[^3][^4] - 托管泄漏:`.NET`相关堆(如`GC Heap`)占比过高[^2] 2. **通用准备步骤** ```bash .symfix # 加载符号表 .loadby sos clr # 加载SOS调试扩展 !analyze -v # 初步分析异常 ``` #### 二、托管内存泄漏分析 1. **定位异常对象** ```bash !dumpheap -stat # 统计对象数量(重点关注TOP10) !dumpheap -mt <MethodTable> # 查看具体对象实例 ``` 2. **追踪引用链** ```bash !gcroot <object_addr> # 显示阻止GC回收的根引用[^2] ``` 3. **终结队列检测** ```bash !finalizequeue # 检查未执行Finalize的对象 ``` #### 三、非托管内存泄漏分析 1. **堆分配分析** ```bash !heap -s # 显示所有堆块统计 !heap -flt s <size> # 过滤特定大小堆块 ``` 2. **内存增长对比** 使用`umdh`工具生成两个时间点的堆快照: ```bash gflags /i MyApp.exe +ust # 启用堆栈跟踪 umdh -pn:MyApp.exe -f:leak1.log umdh -pn:MyApp.exe -f:leak2.log umdh leak1.log leak2.log > cmp.txt ``` 3. **泄漏点定位** ```bash !heap -p -a <address> # 显示堆块分配调用栈 ``` #### 四、混合泄漏场景 1. **检查托管资源包装** 使用`!do`检查托管对象是否持有非托管句柄: ```bash !do <object_addr> ``` 2. **跨边界追踪** 通过`!GCHandles`查看全局句柄表,定位跨域引用[^4] #### 五、关键差异对照表 | 特征 | 托管泄漏 | 非托管泄漏 | |--------------|-----------------------------------|---------------------------| | 内存类型 | CLR堆内存 | 原生堆/系统API分配 | | 分析工具 | SOS扩展命令 | UMDH/!heap命令 | | 常见原因 | 静态引用/事件未解绑 | 未释放句柄/COM对象 | | 内存回收 | 依赖GC机制 | 需显式释放 | | 典型命令 | !dumpheap, !gcroot | !heap, !address | [^1]: 引用自内存泄漏分析工具使用注意事项 [^2]: 引用自托管内存泄漏分析方法 [^3]: 引用自非托管泄漏实战案例 [^4]: 引用自混合泄漏场景分析经验
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值