最近花了好几周解决一个WPF高内存的问题,问题的表象是内存不断增加、未被回收,根源是GC的FinalizeThread被阻塞,导致整个GC挂掉。从以下几步来分析这个问题:

1.用ANTS Memory Profiler去掉强引用

既然是高内存,肯定要先从内存着手。这里必须要赞一下ANTS的这个工具,图形化做的非常好,一目了然,个人觉得比SciTech的.net memory profiler好用。找个基准点take一个SnapShot,打开关闭窗口后再take一个snapshot,比较2个快照里多出了哪些对象,或者窗口对象被什么强引用了导致未被释放,都很清楚。一般来说是自己代码的问题,但也有第三方组件的坑,比如:

  1. DevExpress.Data.DelayedExecutionExtension里有static的Dictionary,会持有很多控件的强引用,需要在窗口Close时调用RemoveDelayedExecute()
  2. TypeDescriptor相关字样的一堆类(包括DPCustomTypeDescriptor、DependencyPropertyDescriptor、DependencyObjectProvider等),都通过DependencyObject._effectiveValues持有对窗口等控件的强引用,需要在窗口Close时调用TypeDescriptor.Refresh(object)

    注:不查不知道,这个TypeDescriptor还大有来头,不仅管理着所有.net object的metadata,还可以动态修改这些metadata,这对于封装一些代理类、提供Transparent功能的场景应该很有用。详情见这篇博客TypeDescriptor

2.当去掉所有强引用后,大量对象堆积在FinalizeQueue上

在ANTS里看到,所有希望回收的对象,都堆积在FinalizeQueue上,即使内存飙到1G也无法回收。手动调GC强制回收,阻塞在WaitForPendingFinalizers()上一直无法返回。

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

这时就只能抓dump用windbg分析到底是哪里卡住了。

3.抓dump分析线程堆栈

用ProcDump -ma [ProcessName]抓dump,分析如下:

/*0:000> .loadby sos clr
0:000> !threads
ThreadCount: 63
UnstartedThread: 0
BackgroundThread: 34
PendingThread: 0
DeadThread: 28
Hosted Runtime: no
Lock
ID OSID ThreadOBJ State GC Mode GC Alloc Context Domain Count Apt Exception
0 1 1b80 00000000008c9cf0 26020 Preemptive 0000000000000000:0000000000000000 000000000087ce90 0 STA
2 2 ba8 00000000008d4790 2b220 Preemptive 0000000000000000:0000000000000000 000000000087ce90 0 MTA (Finalizer)
......
40 63 1748 0000000021ecc3d0 1029220 Preemptive 0000000000000000:0000000000000000 000000000087ce90 0 MTA (Threadpool Worker)*/ 0:000> !syncblk
Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner
-----------------------------
Total 173
CCW 5
RCW 5
ComClassFactory 0
Free 25 //并没有线程被锁住,看看2号Finalizer线程在干嘛
/*0:000> ~2kb
RetAddr : Args to Child : Call Site
000007fe`fcf210dc : 00000000`00000000 00000000`1b66f308 00000000`1b66ee60 00000000`1b66edd0 : ntdll!NtWaitForSingleObject+0xa
000007fe`fd2de68e : 00000000`1bf51bf0 00000000`00911fb0 00000000`00000000 00000000`0000044c : KERNELBASE!WaitForSingleObjectEx+0x79
000007fe`fd413700 : 00000000`008fd0b0 00000000`1bf51bf0 00000000`00000246 00000000`008fd0b0 : ole32!GetToSTA+0x8a
000007fe`fd41265b : 00000000`00000000 00000000`ffffffff 00000000`61a6d4cc 00000000`ffffffff : ole32!CRpcChannelBuffer::SwitchAptAndDispatchCall+0x13b
......
000007fe`e53b383c : 00000000`1bfd2380 00000000`1b66f9a8 00000000`00000000 000007fe`e5417ad3 : clr!CtxEntry::EnterContext+0x232
000007fe`e53b37e6 : 00000000`1b66f9a8 000007fe`e524307c 00000000`008d4790 00000000`1b66f9f0 : clr!RCW::EnterContext+0x3d
000007fe`e544319f : 000007fe`e5b055b0 00000000`008d4790 00000000`008d4790 00000000`00000000 : clr!SyncBlockCache::CleanupSyncBlocks+0xc2
000007fe`e536ab47 : 00000000`00000001 00000000`00000001 00000000`008d4790 00000000`00000000 : clr!Thread::DoExtraWorkForFinalizer+0xdc
000007fe`e52b458c : 0030002e`00340076 00000000`1b66fcc0 00000000`00390031 00000000`00001000 : clr!WKS::GCHeap::FinalizerThreadWorker+0x109
000007fe`e52b451a : 00000000`1b66fcc0 00000000`00000000 0000cd2d`f0a20ef6 000007fe`e53ff57a : clr!Frame::Pop+0x50
...
000007fe`e5391d90 : 00000000`00000000 00000000`00000000 00000000`00000001 00000000`0000001e : clr!ManagedThreadBase_NoADTransition+0x3f
000007fe`e53133de : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : clr!WKS::GCHeap::FinalizerThreadStart+0xb4
00000000`76fa59ed : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : clr!Thread::intermediateThreadProc+0x7d
00000000`770dc541 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d*/ //强烈建议关注一下GetToSTA,这个方法是COM组件引起GC阻塞的典型特征。原因是STA的COM组件必须在创建它的线程上被回收,所以FinalizerThread想GetToSTA线程去执行回收的代码。但它想GetTo的是哪个线程?那个线程又因为什么阻塞了呢?
000007fe`fd413700 : 00000000`008fd0b0 00000000`1bf51bf0 00000000`00000246 00000000`008fd0b0 : ole32!GetToSTA+0x8a //首先用|查看进程ID
0:000> |
. 0 id: 3b8c examine name: E:\...\Process.exe
//然后在这个方法的参数列表上,依次执行dd并查找进程ID:3b8c,进程ID旁边就是它要去的线程ID,我是在第二个参数里找到的。
0:000> dd 1bf51bf0
00000000`1bf51bf0 fd4283e0 000007fe fd450628 000007fe
00000000`1bf51c00 00000000 00000000 00000001 0000102a
00000000`1bf51c10 00000000 00000000 0000044c 00000000
00000000`1bf51c20 00000000 00000000 00003400 1b803b8c
//最后一行的1b80 3b8c,后半段是进程ID、前半段是它要去STA线程。回上面一看,原来就是0号主线程。
0 1 1b80 00000000008c9cf0 26020 Preemptive 0000000000000000:0000000000000000 000000000087ce90 0 STA //再看看0号主线程在干嘛?原来停在ConnectNamedPipe里,对应的.net代码是NamedPipeStreamServer.WaitForConnection()。这是个非托管的阻塞方法,只要等不到Connection,就会一直阻塞。
/*0:000> kb
RetAddr : Args to Child : Call Site
000007fe`fcf33c2f : 000007fe`e5311ed4 00000000`0027e428 00000000`0027e480 00000000`05d5cac8 : ntdll!NtFsControlFile+0xa
*** WARNING: Unable to verify checksum for System.Core.ni.dll
000007fe`e1ab8017 : 000007fe`e169adb8 00000000`00000000 00000000`00000000 00000000`05d5c7d8 : KERNELBASE!ConnectNamedPipe+0x6f
......
000007fe`e53a2a7e : 00000000`00000000 00000000`00000004 00000000`00000000 00000000`00000004 : clr!MethodDescCallSite::CallTargetWorker+0x2e2
000007fe`e53a31d6 : 00000000`00000004 00000000`00000000 00000000`00000000 00000000`03162eb8 : clr!RunMain+0x1e7
000007fe`e53a30d0 : 00000000`008f13b0 00000000`00000200 00000000`008f13b0 00000000`00000200 : clr!Assembly::ExecuteMainMethod+0xb6
000007fe`e53a2c46 : 00000000`0027f8c8 00000000`00bf0000 00000000`00000000 00000000`00000000 : clr!SystemDomain::ExecuteMainMethod+0x45e
000007fe`e53a2b9e : 00000000`00bf0000 00000000`0027fa20 00000000`00000000 000007fe`f6f441c0 : clr!ExecuteEXE+0x3f
000007fe`e53a3574 : ffffffff`ffffffff 00000000`00000000 00000000`00000000 00000000`00000000 : clr!CorExeMainInternal+0xae
000007fe`f6ee77ad : 00000000`00000000 000007fe`00000091 00000000`00000000 00000000`0027f988 : clr!CorExeMain+0x14
000007fe`f6fa5b21 : 00000000`00000000 000007fe`e53a3560 00000000`00000000 00000000`00000000 : mscoreei!CorExeMain+0xe0
00000000`76fa59ed : 000007fe`f6ee0000 00000000`00000000 00000000`00000000 00000000`00000000 : mscoree!CorExeMain_Exported+0x57
00000000`770dc541 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d*/

怪不得FinalizerThread会挂掉,因为它要释放COM组件,所以要进到创建COM组件的STA线程,而这个STA线程又在无限期的等待Connection,所以就挂掉了。GC都挂了,永远不执行垃圾回收,当然会高内存。另:GetToSTA的手法太妖了,这么hack的方法我当然想不出来,请参见gcHang0gcHang1gcHang2gcHang3

4.解决方案

  1. 把创建COM的线程改为MTA,因为主线程必须是STA的,所以只能新建一个MTA的线程来干这个事儿了。
  2. 如果非得在STA的线程里干这事儿,那就不能使用非托管的阻塞方法,比如WaitForConnection,而要使用托管的阻塞方法,比如WaitHandle.WaitOne, WaitAny, WaitAll, Monitor.Enter, Monitor.Block, Thread.Join, GC.WaitForPendingFinalizers这些都是,这些方法在阻塞线程的同时,还能正确的pump messages。这些托管的阻塞方法,配合对应的Begin/End异步方法,就能响应各种消息了。

5.演示的Demo

最后用一个小Demo把问题重现了一遍,也把解决方案附在里面了,有兴趣的同学可以试一下。

debug实战:COM组件GetToSTA导致高内存+GC被阻塞的更多相关文章

  1. debug实战:Unmanaged High Memory非托管高内存

    最近又监控到一个高内存的问题,周五下班把系统打开,周末2天没关,周一来看已经涨到5.2G,这次与以往不同,不是.net的内存泄漏,而是非托管引起的. 1. 抓dump,确定高内存的类型 //dump有 ...

  2. dotnet 6 在 Win7 系统证书链错误导致 HttpWebRequest 内存泄露

    本文记录我将应用迁移到 dotnet 6 之后,在 Win7 系统上,因为使用 HttpWebRequest 访问一个本地服务,此本地服务开启 https 且证书链在此 Win7 系统上错误,导致应用 ...

  3. mysql在高内存、IO利用率上的几个优化点 (sync+fsync) 猎豹移动技术博客

    http://dev.cmcm.com/archives/107 Posted on 2014年10月16日 by liuding | 7条评论 以下优化都是基于CentOS系统下的一些优化整理,有不 ...

  4. Windbg分析高内存占用问题

    1. 问题简介 最近产品发布大版本补丁更新,一商超客户升级后,反馈系统经常奔溃,导致超市的收银系统无法正常收银,现场排队付款的顾客更是抱怨声声.为了缓解现场的情况, 客户都是手动回收IIS应用程序池才 ...

  5. MySQL在高内存、IO利用率上的几个优化点

    以下优化都是基于CentOS系统下的一些MySQL优化整理,有不全或有争议的地方望继续补充完善. 一.mysql层面优化 1. innodb_flush_log_at_trx_commit 设置为2设 ...

  6. 精华阅读第 13 期 |常见的八种导致 APP 内存泄漏的问题

    本期是移动开发精英俱乐部的第13期文章,都是以技术为主,所以这里就不过多的进行赘述了,我们直接看干货内容吧!本文系ITOM管理平台OneAPM整理. 实际项目中的MVVM(积木)模式–序章 导读:开篇 ...

  7. 解决微信小程序视频组件层级过高的问题

    本文首发于我的个人博客:http://www.fogcrane.org 前言 在微信小程序的开发中,总有一些"VIP"组件,他们的层级,高得让人抓狂,总是凌驾于很多其他低层级组件之 ...

  8. 引入OpenCV导致私有内存巨大

    引入OpenCV导致私有内存巨大 opencvC++VS2015 说明 在调试程序的时候 发现自己的程序在VS的调试窗口占用很高, 花时间关注了一下这个问题, 手动写了小的程序复现这个问题,最终确定了 ...

  9. JavaScript之详述闭包导致的内存泄露

    一.内存泄露 1. 定义:一块被分配的内存既不能使用,也不能回收.从而影响性能,甚至导致程序崩溃. 2. 起因:JavaScript的垃圾自动回收机制会按一定的策略找出那些不再继续使用的变量,释放其占 ...

随机推荐

  1. 安装第三方库出现 Python version 2.7 required, which was not found in the registry

    安装第三方库出现 Python version 2.7 required, which was not found in the registry 建立一个文件 register.py 内容如下. 然 ...

  2. python对json的操作总结

    Json简介:Json,全名 JavaScript Object Notation,是一种轻量级的数据交换格式.Json最广泛的应用是作为AJAX中web服务器和客户端的通讯的数据格式.现在也常用于h ...

  3. innodb必收藏

    http://www.xaprb.com/blog/2015/08/08/innodb-book-outline/

  4. Oracle数据库DOC命令导入导出(2014-3-10记)

    导出:exp lwj/lwj123456@orcl file=d:/db.dmp full=y 导入:imp lwj/lwj123456@orcl file=d:/db.dmp full=y 注:用户 ...

  5. Black World

  6. js !!条件判断或运算的作用

    今天看到一个判断语句非常奇怪: if(!!selected){} //为什么是双'!'号呢? 自己查了下资料终于明白了这其中的原理: 原来'!!'会将表达式转为Boolean类型的数据. 如果'!un ...

  7. Walls(floyd POJ1161)

    Walls Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 7677   Accepted: 3719 Description ...

  8. 利用excel拆分数据

    要求:将sheet1中的数据按照公司名称拆分到不同的工作表 使用VBA: 1:打开sheet1的查看代码 2:运行 ·········································· ...

  9. python 中的input

    渣渣之路. 一. 在python编程初学者指南中的第六章.使用参数和返回值的例子中: # -*- coding: utf-8 -*- def display(message): print messa ...

  10. static代码块与{}代码块的比较

    第一个例子: public class StaticDemo { { System.out.println("{} 代码块"); } static{ System.out.prin ...