記一次 .NET 某列印服務 非托管記憶體泄漏分析

来源:https://www.cnblogs.com/huangxincheng/archive/2022/09/14/16691715.html
-Advertisement-
Play Games

一:背景 1. 講故事 前段時間有位朋友在微信上找到我,說他的程式出現了記憶體泄漏,能不能幫他看一下,這個問題還是比較經典的,加上好久沒上非托管方面的東西了,這篇就和大家分享一下,話不多說,上 WinDbg 說話。 二:WinDbg 分析 1. 到底是哪裡的泄漏 好的開始就是成功的一半,否則就南轅北轍 ...


一:背景

1. 講故事

前段時間有位朋友在微信上找到我,說他的程式出現了記憶體泄漏,能不能幫他看一下,這個問題還是比較經典的,加上好久沒上非托管方面的東西了,這篇就和大家分享一下,話不多說,上 WinDbg 說話。

二:WinDbg 分析

1. 到底是哪裡的泄漏

好的開始就是成功的一半,否則就南轅北轍了,對吧,還是用經典的 !address -summary 看一下記憶體排布情況。


0:000> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Heap                                   1935          553b3000 (   1.332 GB)  70.57%   66.59%
Image                                  1022           c306000 ( 195.023 MB)  10.09%    9.52%
<unknown>                              1202           c09d000 ( 192.613 MB)   9.97%    9.41%
Stack                                   541           b280000 ( 178.500 MB)   9.24%    8.72%
Free                                   1158           73ab000 ( 115.668 MB)            5.65%
TEB                                     180            20f000 (   2.059 MB)   0.11%    0.10%
Other                                     8             5d000 ( 372.000 kB)   0.02%    0.02%
PEB                                       1              3000 (  12.000 kB)   0.00%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_COMMIT                             3077          643c6000 (   1.566 GB)  83.00%   78.31%
MEM_RESERVE                            1812          1487f000 ( 328.496 MB)  17.00%   16.04%
MEM_FREE                               1158           73ab000 ( 115.668 MB)            5.65%

從卦中可以看出,當前 MEM_COMMIT = 1.56 G, 並且 Heap= 1.3 G,既然超出了朋友的預期,很明顯這是一個非托管記憶體泄漏,既然 NTHeap 出現了泄漏,那就挖一下看看,使用 !heap -s 觀察一下各個heap句柄。


0:000> !heap -s

************************************************************************************************************************
                                              NT HEAP STATS BELOW
************************************************************************************************************************
LFH Key                   : 0xbb72f2a3
Termination on corruption : DISABLED
  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast 
                    (k)     (k)    (k)     (k) length      blocks cont. heap 
-----------------------------------------------------------------------------
00770000 00000002   16576   9716  16364     33   195     5    0      0   LFH
006f0000 00001002    1292    148   1080     11     4     2    0      0   LFH
00a80000 00001002    3336   1972   3124     88    25     3    0      0   LFH
02460000 00001002      60      4     60      0     1     1    0      0      
023b0000 00041002      60      4     60      2     1     1    0      0      
02450000 00001002     272     24     60      1     3     1    0      0   LFH
04a40000 00041002    1292     80   1080      8     4     2    0      0   LFH
06e90000 00001002   64180  56660  63968   1434   473     9  624      7   LFH
09dc0000 00001002      60     12     60      3     2     1    0      0      
0a500000 00001002    7428   3772   7216     43    35     4    0      0   LFH
-----------------------------------------------------------------------------

從卦中的 Commit 列來看,記憶體占用都不大,最大的也不過 56M ,如果經驗豐富的話,你會發現 Virt blocks 高達 624 個,明白 ntheap 的朋友應該知道,凡是大於 512kheapentry 都會單獨安排到 VirtualAllocdBlocks 數組中,可以用 dt ntdll!_HEAP 06e90000 給show出來。


0:000> dt ntdll!_HEAP 06e90000
   ...
   +0x05c VirtualMemoryThreshold : 0xfe00
   +0x09c VirtualAllocdBlocks : _LIST_ENTRY [ 0x6ea4000 - 0x7c0d0000 ]
   ...

為了更好的輸出 VirtualAllocdBlocks 數組,我們用 windbg 自帶的 heap 分析命令。


0:000> !heap 06e90000 -m
Index   Address  Name      Debugging options enabled
  8:   06e90000 
    Segment at 06e90000 to 06e9f000 (0000f000 bytes committed)
    Segment at 078f0000 to 079ef000 (000ff000 bytes committed)
    Segment at 08870000 to 08a6f000 (001ff000 bytes committed)
    Segment at 0ec60000 to 0f05f000 (003f9000 bytes committed)
    Segment at 18660000 to 18e5f000 (007fa000 bytes committed)
    Segment at 26b20000 to 27aef000 (00fc0000 bytes committed)
    Segment at 45320000 to 462ef000 (00fcf000 bytes committed)
    Segment at 65bf0000 to 66bbf000 (008bf000 bytes committed)
    Flags:                00001002
    ForceFlags:           00000000
    Granularity:          8 bytes
    Segment Reserve:      03f70000
    Segment Commit:       00002000
    DeCommit Block Thres: 00000800
    DeCommit Total Thres: 00002000
    Total Free Size:      0002cd56
    Max. Allocation Size: 7ffdefff
    Lock Variable at:     06e90258
    Next TagIndex:        0000
    Maximum TagIndex:     0000
    Tag Entries:          00000000
    PsuedoTag Entries:    00000000
    Virtual Alloc List:   06e9009c
        06ea4000: 00200000 [commited 201000, unused 1000] - busy (b)
        070b2000: 00200000 [commited 201000, unused 1000] - busy (b)
        079f4000: 00200000 [commited 201000, unused 1000] - busy (b)
        07c0f000: 00200000 [commited 201000, unused 1000] - busy (b)
        0802b000: 00200000 [commited 201000, unused 1000] - busy (b)
        08238000: 00200000 [commited 201000, unused 1000] - busy (b)
        08444000: 00200000 [commited 201000, unused 1000] - busy (b)
        0865f000: 00200000 [commited 201000, unused 1000] - busy (b)
        0e20f000: 00200000 [commited 201000, unused 1000] - busy (b)
        0e42b000: 00200000 [commited 201000, unused 1000] - busy (b)
        0e635000: 00200000 [commited 201000, unused 1000] - busy (b)
        0e841000: 00200000 [commited 201000, unused 1000] - busy (b)
        0c661000: 00200000 [commited 201000, unused 1000] - busy (b)
        0c87e000: 00200000 [commited 201000, unused 1000] - busy (b)
        0ca8b000: 00200000 [commited 201000, unused 1000] - busy (b)
        0ea56000: 00200000 [commited 201000, unused 1000] - busy (b)
        0f062000: 00200000 [commited 201000, unused 1000] - busy (b)
        0f275000: 00200000 [commited 201000, unused 1000] - busy (b)
        ...

從卦中可以看到大量的 commited 201000, unused 1000 ,這裡的 0x201000 轉換一下大概就是 2M,以經驗來說,這 2M 大概就是 pdf,image,bitmap 等這些玩意了,由於沒有開啟 pageheap 或 ust,沒法追蹤到底是什麼東西分配的,到這裡就沒法進展下去了。

2. 到底是誰分配的 2M 數據

首先能進入 VirtualAllocdBlocks 數組自然是高層調用了 HeapAlloc 這類API,同時這個數據量高度懷疑是 Bitmap,Pdf 之類的大文件,很大可能是托管代碼做了什麼導致這個資源沒有釋放,接下來使用 !dumpheap -stat 看下托管堆。


0:000> !dumpheap -stat
Statistics:
      MT    Count    TotalSize Class Name
...
09ae7e48      627        15048 System.Drawing.Bitmap
6b267040      178       366680 System.Decimal[]
6b2cb4a0     1850       601588 System.String[]
6b2cdd14     1379       638190 System.Byte[]
6b2cac14    15919      1146764 System.String
09aec720    66332      1326640 System.Drawing.FontFamily
09ae8590    66074      2907256 System.Drawing.Font
Total 289300 objects

從卦中看,System.Drawing.Font 居然高達 6w 個,而且 System.Drawing.Bitmap 和 heap 上的 624 也非常接近,看樣子就是 Bitmap 啦,那為什麼這個 Bitmap 沒有善終呢? 可以用 !frq -stat 觀察下終結器隊列的 Freachable Queue 情況。


0:000> !frq -stat
Freachable Queue:
       Count      Total Size   Type
---------------------------------------------------------
         152            3648   System.Data.Odbc.CNativeBuffer
          76            2128   System.Data.Odbc.OdbcConnectionHandle
          77            1540   System.Transactions.SafeIUnknown
          76            1824   System.Data.Odbc.OdbcStatementHandle
        2432          145920   System.Windows.Forms.Control+ControlNativeWindow
         304            7296   System.Drawing.Bitmap
       66062         2906728   System.Drawing.Font
         258            5160   System.Drawing.FontFamily
         308            9856   System.Drawing.Graphics
         308            3696   System.Windows.Forms.ImageList+NativeImageList
           1              12   System.Drawing.Text.InstalledFontCollection
          12             240   System.Threading.ThreadPoolWorkQueueThreadLocals
           1              20   System.Security.Cryptography.SafeKeyHandle
           1              20   Microsoft.Win32.SafeHandles.SafeWaitHandle
           6             120   Microsoft.Win32.SafeHandles.SafeRegistryHandle
          12             624   System.Threading.Thread
        1577           69388   System.Threading.ReaderWriterLock
           1              20   System.Security.Cryptography.SafeProvHandle

71,664 objects, 3,158,240 bytes

我去,這個可終結隊列居然高達 7.1w ,這是很有問題的,大概率當前的終結器線程瓦特了,接下來追查下 終結器線程 此時正在做什麼 ?


0:000> !t
ThreadCount:      107
UnstartedThread:  0
BackgroundThread: 93
PendingThread:    0
DeadThread:       12
Hosted Runtime:   no
                                                                         Lock  
       ID OSID ThreadOBJ    State GC Mode     GC Alloc Context  Domain   Count Apt Exception
   0    1 138ac 0079fef0     26020 Preemptive  00000000:00000000 00798f38 1     STA 
   2    2 12b08 007adac0     2b220 Preemptive  00000000:00000000 00798f38 0     MTA (Finalizer) 
  ...

0:000> ~2s
eax=00000001 ebx=00000000 ecx=00000000 edx=00000000 esi=00000001 edi=00000001
eip=777b2f8c esp=0466eaf4 ebp=0466ec84 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!NtWaitForMultipleObjects+0xc:
777b2f8c c21400          ret     14h
0:002> k
 # ChildEBP RetAddr      
00 0466ec84 762fc753     ntdll!NtWaitForMultipleObjects+0xc
01 0466ec84 7695d9aa     KERNELBASE!WaitForMultipleObjectsEx+0x103
02 0466ed34 7695c564     combase!MTAThreadWaitForCall+0x1ca [onecore\com\combase\dcomrem\channelb.cxx @ 7234] 
03 0466edc0 769a9923     combase!MTAThreadDispatchCrossApartmentCall+0xf4 [onecore\com\combase\dcomrem\chancont.cxx @ 229] 
04 (Inline) --------     combase!CSyncClientCall::SwitchAptAndDispatchCall+0x9e4 [onecore\com\combase\dcomrem\channelb.cxx @ 5856] 
05 0466efa0 769ab739     combase!CSyncClientCall::SendReceive2+0xad3 [onecore\com\combase\dcomrem\channelb.cxx @ 5459] 
06 (Inline) --------     combase!SyncClientCallRetryContext::SendReceiveWithRetry+0x29 [onecore\com\combase\dcomrem\callctrl.cxx @ 1542] 
07 (Inline) --------     combase!CSyncClientCall::SendReceiveInRetryContext+0x29 [onecore\com\combase\dcomrem\callctrl.cxx @ 565] 
...

從上面的 MTAThreadDispatchCrossApartmentCall 可知,這又是一個經典的 COM 釋放問題導致終結器線程卡死。。。接下來仔細看下 線程列表的 STA 情況,可以發現有大量的線程是 STA 模式。

接下來就是將結果告訴朋友,為什麼開的線程都是 STA 套件模式。

三:總結

總的來說,這次記憶體泄漏的原因在於朋友開了 STA 模式的線程,導致終結器線程卡死,進而導致 Bitmap 分配之後無法釋放,最終引發非托管泄漏。

這個dump告訴我們,不要放棄,一定可以在絕望中找到希望。

圖片名稱
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、SQL中limit的基本用法 我們先來熟悉SQL中limit的基本用法 這是我現有的表結構 然後進行limit查詢 1. select * from user limit 3,4 這句SQL語句的意思是查詢user表,跳過前3行,也就是從第四行開始查詢4行數據。查詢結果如下: 2. select ...
  • 前言 嗨嘍,大家好呀~這裡是愛看美女的茜茜吶 又到了學Python時刻~ 你還在為一個一個下載壁紙而煩惱嗎,那有沒有更加簡單的方法呢? 跟著我,一起來看看我是如何批量下載的吧 環境使用: python3.8 | Anaconda pycharm 相關模塊: requests >>> pip inst ...
  • 業務模塊介紹 現在我們對整體的業務進行介紹以及演示 5. 全鏈路整體架構 上面介紹了為什麼需要全鏈路壓測,下麵來看下全鏈路壓測的整體架構。 ​ 整體架構如下主要是對壓測客戶端的壓測數據染色,全鏈路中間件識別出染色數據,並將正常數據和壓測數據區分開,進行數據隔離,這裡主要涉及到mysql資料庫,Rab ...
  • MyBatis 通過使用內置的日誌工廠提供日誌功能。 在這裡我們對STDOUT_LOGGING和LOG4J進行學習。 一、STDOUT_LOGGING 1.什麼是STDOUT_LOGGING STDOUT_LOGGING是MyBatis的標準日誌配置。STDOUT_LOGGING的使用無需其他的依賴 ...
  • 從提升性能角度來說 提升了對CPU的使用效率:目前生產的伺服器大多數都是多核,標配的機器都是 8C/16G。操作系統會將不同的線程分配給不同的核心處理,理論上,有多少核心就有多少個線程並行執行。如果沒有併發編程,CPU的利用率將極大的浪費,假設當前正在處理耗時的 I/O 操作,那麼整個CPU就會處於... ...
  • 大家好,我是三友~~ 在對於讀寫鎖的認識當中,我們都認為讀時加讀鎖,寫時加寫鎖來保證讀寫和寫寫互斥,從而達到讀寫安全的目的。但是就在我翻Eureka源碼的時候,發現Eureka在使用讀寫鎖時竟然是在讀時加寫鎖,寫時加讀鎖,這波操作屬實震驚到了我,於是我就花了點時間研究了一下Eureka的這波操作。 ...
  • DotnetZip使用方法見此文章https://www.cnblogs.com/pengze0902/p/6124659.html在netframework環境下,使用上面文章中的設置Encoding為Default的方法即可解決中文亂碼問題 但是當我使用.net6創建控制台項目並採用上述代碼時, ...
  • iNeuOS工業互聯網操作系統面向:儀器儀錶、雙碳環保、核能科學與工程和鋼鐵冶金領域頒發第一批技術認證資質,一共21名同志在項目實施過程中表現突出,從iNeuOS的應用、開發及項目過程中的交流都大大促進了項目保質保量的快速交付,特此頒發應用實施和二次開發工程認證。 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...