debug实战:COM组件GetToSTA导致高内存+GC被阻塞
最近花了好几周解决一个WPF高内存的问题,问题的表象是内存不断增加、未被回收,根源是GC的FinalizeThread被阻塞,导致整个GC挂掉。从以下几步来分析这个问题:
1.用ANTS Memory Profiler去掉强引用
既然是高内存,肯定要先从内存着手。这里必须要赞一下ANTS的这个工具,图形化做的非常好,一目了然,个人觉得比SciTech的.net memory profiler好用。找个基准点take一个SnapShot,打开关闭窗口后再take一个snapshot,比较2个快照里多出了哪些对象,或者窗口对象被什么强引用了导致未被释放,都很清楚。一般来说是自己代码的问题,但也有第三方组件的坑,比如:
- DevExpress.Data.DelayedExecutionExtension里有static的Dictionary,会持有很多控件的强引用,需要在窗口Close时调用RemoveDelayedExecute()
- 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的方法我当然想不出来,请参见gcHang0、gcHang1、gcHang2、gcHang3
4.解决方案
- 把创建COM的线程改为MTA,因为主线程必须是STA的,所以只能新建一个MTA的线程来干这个事儿了。
- 如果非得在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被阻塞的更多相关文章
- debug实战:Unmanaged High Memory非托管高内存
最近又监控到一个高内存的问题,周五下班把系统打开,周末2天没关,周一来看已经涨到5.2G,这次与以往不同,不是.net的内存泄漏,而是非托管引起的. 1. 抓dump,确定高内存的类型 //dump有 ...
- dotnet 6 在 Win7 系统证书链错误导致 HttpWebRequest 内存泄露
本文记录我将应用迁移到 dotnet 6 之后,在 Win7 系统上,因为使用 HttpWebRequest 访问一个本地服务,此本地服务开启 https 且证书链在此 Win7 系统上错误,导致应用 ...
- mysql在高内存、IO利用率上的几个优化点 (sync+fsync) 猎豹移动技术博客
http://dev.cmcm.com/archives/107 Posted on 2014年10月16日 by liuding | 7条评论 以下优化都是基于CentOS系统下的一些优化整理,有不 ...
- Windbg分析高内存占用问题
1. 问题简介 最近产品发布大版本补丁更新,一商超客户升级后,反馈系统经常奔溃,导致超市的收银系统无法正常收银,现场排队付款的顾客更是抱怨声声.为了缓解现场的情况, 客户都是手动回收IIS应用程序池才 ...
- MySQL在高内存、IO利用率上的几个优化点
以下优化都是基于CentOS系统下的一些MySQL优化整理,有不全或有争议的地方望继续补充完善. 一.mysql层面优化 1. innodb_flush_log_at_trx_commit 设置为2设 ...
- 精华阅读第 13 期 |常见的八种导致 APP 内存泄漏的问题
本期是移动开发精英俱乐部的第13期文章,都是以技术为主,所以这里就不过多的进行赘述了,我们直接看干货内容吧!本文系ITOM管理平台OneAPM整理. 实际项目中的MVVM(积木)模式–序章 导读:开篇 ...
- 解决微信小程序视频组件层级过高的问题
本文首发于我的个人博客:http://www.fogcrane.org 前言 在微信小程序的开发中,总有一些"VIP"组件,他们的层级,高得让人抓狂,总是凌驾于很多其他低层级组件之 ...
- 引入OpenCV导致私有内存巨大
引入OpenCV导致私有内存巨大 opencvC++VS2015 说明 在调试程序的时候 发现自己的程序在VS的调试窗口占用很高, 花时间关注了一下这个问题, 手动写了小的程序复现这个问题,最终确定了 ...
- JavaScript之详述闭包导致的内存泄露
一.内存泄露 1. 定义:一块被分配的内存既不能使用,也不能回收.从而影响性能,甚至导致程序崩溃. 2. 起因:JavaScript的垃圾自动回收机制会按一定的策略找出那些不再继续使用的变量,释放其占 ...
随机推荐
- Java使用占位符拼接字符串
大家知道,在C#编程中,可以用占位符来拼接字符串,用起来非常的方便. 特别是需要进行大量的参数拼接的时候,比如: Console.WriteLine(String.Format("该域名{0 ...
- mysql之show engine innodb status解读
注:以下内容为根据<高性能mysql第三版>和<mysql技术内幕innodb存储引擎>的innodb status部分的个人理解,如果有错误,还望指正!! innodb存 ...
- popUpWindow 动画无法超出窗体的解决方案
popupWindow 做动画时,当要求有一个放大动画时,动画无法超出窗体,给人的感觉是只有内容在放大,窗体不动. 这是由于窗口大小固定的原因,解决方案是加大popUpwindow的 大小. 一个比较 ...
- 管道函数(%>%)很简单
%>%来自dplyr包的管道函数,其作用是将前一步的结果直接传参给下一步的函数,从而省略了中间的赋值步骤,可以大量减少内存中的对象,节省内存 符号%>%,这是管道操作,其意思是将%> ...
- C++高精度计算代码运行时间(转载)
转载:http://blog.csdn.net/rrrfff/article/details/6583410 //在定时前应该先调用QueryPerformanceFrequency()函数获得机器内 ...
- [问题2014S11] 复旦高等代数II(13级)每周一题(第十一教学周)
[问题2014S11] 设 \(A,B\) 为 \(n\) 阶实对称阵, \(p(A),p(B),p(A+B)\) 分别为 \(A,B,A+B\) 的正惯性指数, 证明: \[p(A)+p(B)\g ...
- FreeSWITCH中文语音包
一.中文语音资源的获取 官方提供的资源:http://files.freeswitch.org/releases/sounds/ 自己录音 实在不行可以@我给你发一份. 二.中文资源的安装 英文资源的 ...
- InfoSet
声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...
- 5----table类型
table类型是非常重要的Lua数据类型,也是Lua唯一能描述数据结构的类型 table类型可以很灵活的描述多种数据结构,其本身是基于键值对的形式存储数据的 字典结构 字典结构的table 的两种创建 ...
- 《BI那点儿事》运用标准计分和离差——分析三国超一流统帅综合实力排名 绝对客观,数据说话
数据分析基础概念:标准计分: 1.无论作为变量的满分为几分,其标准计分的平均数势必为0,而其标准差势必为1.2.无论作为变量的单位是什么,其标准计分的平均数势必为0,而其标准差势必为1.公式为: 离差 ...