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的垃圾自动回收机制会按一定的策略找出那些不再继续使用的变量,释放其占 ...
随机推荐
- paper 110:凸优化和非凸优化
数学中最优化问题的一般表述是求取,使,其中是n维向量,是的可行域,是上的实值函数.凸优化问题是指是闭合的凸集且是上的凸函数的最优化问题,这两个条件任一不满足则该问题即为非凸的最优化问题. 其中,是 凸 ...
- css3 flex
<!DOCTYPE html> <html> <head> <style> .first-face { display: flex; justify-c ...
- Fiddler 前端工具
官网地址:http://www.telerik.com/fiddler 支持所有平台,但由于早期是基于.net开发的,对window操作系统支持较好,对mac,linux支持不够好.
- 关于SQL Server无法查询中文的问题
SQL Server 的版本是2016,随便试了一条 带有 where 子句的查询.如下: select * from Roles where RoleName like '%系统%' 呐尼,怎么一条 ...
- .NET 使用CouchBase 基础篇
2011年2月,CouchOne和memebase合并后,改名为Couchbase,官网地址(www.couchbase.com).membase最后一个版本为1.7.2,可在Couchbase的官网 ...
- SQL2008根据日志恢复
--创建测试数据库 CREATE DATABASE Db GO --对数据库进行备份 BACKUP DATABASE Db TO DISK='c:\db.bak' WITH FORMAT GO --创 ...
- Python—模块
一.模块 模块,是用一堆代码实现了某个功能的代码集合,模块分为三种:自定义模块(自己定义).内置模块(python自带).开源模块 导入模块 (1).导入一个py文件,解释器解释该py文件 (2).导 ...
- 如何清理photoshop cs6 被升级的烦人的adobe creative cloud组件
安装photoshop cs6(虽然目前已经退出到cc 2015,不过因激活成熟度等,我还是偏向于使用cs6,够用!),默认安装adobe application manager. 不过如果不小心单独 ...
- tomcat 内存溢出解决办法
- Auty自动化测试框架第四篇——生成测试结果报告
[本文出自天外归云的博客园] 本次为Auty框架添加生成测试结果报告功能,文件结构更新: