記一次 .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
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...