debug實戰:COM組件GetToSTA導致高記憶體+GC被阻塞

来源:http://www.cnblogs.com/AlexanderYao/archive/2016/02/16/5192358.html
-Advertisement-
Play Games

最近花了好幾周解決一個WPF高記憶體的問題,問題的表象是記憶體不斷增加、未被回收,根源是GC的FinalizeThread被阻塞,導致整個GC掛掉。從以下幾步來分析這個問題: 1.用ANTS Memory Profiler去掉強引用 既然是高記憶體,肯定要先從記憶體著手。這裡必須要贊一下ANTS的這個工具,


最近花了好幾周解決一個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把問題重現了一遍,也把解決方案附在裡面了,有興趣的同學可以試一下。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 當管理SQL Server內在的帳戶和密碼時,我們很容易認為這一切都相當的安全。但實際上並非如此。在這裡,我們列出了一些對於SQL Server密碼來說非常危險的判斷。 當管理SQL Server內在的帳戶和密碼時,我們很容易認為這一切都相當的安全。畢竟,你的SQL Server系統被保護在防火牆裡
  • 一.概述 netfilter是自2.4內核的一個數據包過濾框架。可以過濾數據包,網路地址和埠轉換(nat和napt技術),以及其他操作數據包的功能。主要工作原理是在內核模塊註冊回調函數(hook函數)到內核,內核執行到相關點時會觸發這個回調函數,然後根據回調函數里的邏輯,對包含網路協議棧的sk_b
  • 使用了VIM這麼久,卻一直無法牢記一些基本的操作指令。今天查找一個關鍵字時,想不起來怎麼查找“下一個”,於是google之並解決,順便把有用的都貼過來罷。查找指令:/xxx 往下查找?xxx 往上 n 下一個:set hls 打開高亮:set nohls 關閉高亮下麵是查找替換,雖然我至今沒使用過這
  • 下載 http://pan.baidu.com/s/1eRkEegM 解壓 終端中切換到下載文件的目錄下,執行以下命令: sudo tar -jxvf sublime_text_3_build_3083_x64.tar.bz2 sudo mkdir /opt/sublime_text_3 sudo
  • Linux系統中的wget是一個下載文件的工具,它用在命令行下。對於Linux用戶是必不可少的工具,我們經常要下載一些軟體或從遠程伺服器恢復備份到本地伺服器。wget支持HTTP,HTTPS和FTP協議,可以使用HTTP代理。所謂的自動下載是指,wget可以在用戶退出系統的之後在後臺執行。這意味這你...
  • 系統安裝 安裝準備 系統:fedora 、Win 10 硬體:U盤一枚、PC一臺 軟體:UltraISO 安裝步驟 使用UltraISO將鏡像寫入U盤 window10使用磁碟管理,空出一個未分配的區域留給fedora安裝系統,一般20G以上 重起電腦,bios中修改啟動項至usb啟動 出現三個選擇
  • 1.預設配置: 1>允許匿名用戶和本地用戶登陸。 anonymous_enable=YES local_enable=YES 2>匿名用戶使用的登陸名為ftp或anonymous,口令為空;匿名用戶不能離開匿名用戶家目錄/var/ftp,且只能下載不能上傳。 3>本地用戶的登錄名為本地用戶名,口令為...
  • 中文:必須至少有一個對象實現 IComparable。 序列排序時報這個錯誤 lstReports.OrderBy(r => new { r.DepartmentName, r.ReportNo }).ToList(); //error occured 在LINQ to SQL/Entity中可以這
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...