在某次巡查生產環境監控數據的時候,發現某個程序的內存佔用偏高(大於500M)。對於這個程序的做用須要簡單交代一下,這個程序是用作通信服務程序,經過Socket與IOT設備進行通信。由於瞭解這個程序的使用場景,因此對於該程序的內存佔用偏高產生了懷疑。該程序服務的設備並很少,可是佔用了幾百兆的內存,很明顯是存在問題的。html
對於該進程隨後進行的分析也驗證了這個想法,因爲這個問題相對來講比較典型,所以比較具備分享價值,經過對於該案例的分享但願可讓更多人瞭解和掌握內存泄漏問題分析的通常方法。數據庫
內存泄漏問題的分析能夠分爲三大部分:數組
確認問題即確認內存確實存在泄漏問題,這個步驟不是光看看就能夠,還須要儘可能的保留問題發生的現場。不論是什麼樣的內存泄漏問題,最好可以保留內存鏡像用於分析(dump文件),由於內存泄漏問題有時候是瞬間的,若是不及時保留現場,等到有時間看的時候,可能程序已經恢復正常。保存內存鏡像文件的時候最好能夠間隔一段時間保留多個鏡像文件用於對比分析,能夠更好的定位問題。服務器
經過windbg擴展項sos,分析dump文件中的句柄和內存裏面的對象類型。sos隨着.net framework一塊兒安裝,能夠適用於大多數狀況下的調試。網絡
首先檢查內存中的對象統計信息,輸入!dumpheap -stat
:app
0:000> !dumpheap -stat Statistics: MT Count TotalSize Class Name …… 6c3ab8d4 806 38688 System.RuntimeMethodInfoStub 6c363e90 2592 39424 System.RuntimeType[] 6b68105c 2265 45300 System.Net.SafeCloseSocket+InnerSafeCloseSocket 6b680f2c 2265 45300 System.Net.SafeNativeOverlapped 6c36d120 476 45696 System.Reflection.Emit.DynamicILGenerator 08eb8b40 334 49432 Newtonsoft.Json.Serialization.JsonProperty 6b671564 2264 54336 System.Net.Sockets.OverlappedCache 6c3a1dd8 1284 87312 System.Reflection.RuntimeParameterInfo 6c3a1d90 2092 92048 System.Signature …… 6c3a17a8 7179 114864 System.Int64 00d8a37c 10717 900228 ********.NetCommunicator.SocketConnectionInfo 6b674f28 10741 988172 System.Net.Sockets.Socket 6c35da78 88000 1056000 System.Object 08eb08c0 17403 1113792 Newtonsoft.Json.Linq.JProperty 082188ac 10717 1457512 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.String, mscorlib],[*******.RoadGate.API.Entity.MessagePacketModel, ***.RoadGate.API.Entity]][] 6c35e0e4 472 68400932 System.Char[] 6c35d6d8 126896 198551106 System.String 00af6ca0 34283 464007622 Free 6c361d04 25662 1037503288 System.Byte[]
獲取到內存中對象的統計信息後重點關注堆棧中數量較多的類型,經過分析發現內存中有一萬多個socket對象,還有一萬多個放在ConcurrentDictionary中的業務自定義的實體類對象。因爲當前分析的程序是通信服務器,socket的合理值很容易經過分析dump時刻的業務量獲得結果(在本案例中確定是不合理的)。socket
通過諮詢得知當前通信服務的通信對象遠遠達不到上萬客戶端的水平,所以很明顯是socket相關的對象的處理出現了問題,出現了泄漏問題。對於.net程序來講,socket相關對象屬於非託管資源,非託管資源的使用原則上必須顯式地進行釋放或關閉操做。工具
對於應用建立的大多數對象,能夠依賴 .NET 垃圾回收器來進行內存管理。 可是,若是建立包含非託管資源的對象,則當你使用完非託管資源後,必須顯式釋放這些資源。 最經常使用的非託管資源類型是包裝操做系統資源的對象,如文件、窗口、網絡鏈接或數據庫鏈接。 雖然垃圾回收器能夠跟蹤封裝非託管資源的對象的生存期,但沒法瞭解如何發佈並清理這些非託管資源。操作系統
雖然已經定位到通信服務對於socket的處理不當,可是非託管資源究竟是由於未能顯示執行dispose方法致使的問題,仍是說這些對象一直被引用而沒法被回收?想要對於非託管資源的問題進行詳細分析,可使用!finalizequeue
命令進行分析。該命令有三個可選參數:.net
-detail
:顯示須要清理的任何 SyncBlocks 的額外信息,以及有關等待清理的任何 RuntimeCallableWrappers (RCW) 的額外信息,這個選項也是默認值。-allReady
:選項顯示全部準備終止的對象,不管它們已被垃圾回收標記成這樣,仍是將被下一個垃圾回收標記。 「準備終止」列表中的對象爲再也不爲根的可終止對象。-short
:將輸出限制爲每一個對象的地址,能夠跟-allReady
或者-detail
一塊兒使用。首先輸入!finalizequeue -allready
檢查有多少能夠回收的對象:
0:000> !finalizequeue -allready SyncBlocks to be cleaned up: 0 Free-Threaded Interfaces to be released: 0 MTA Interfaces to be released: 0 STA Interfaces to be released: 0 ---------------------------------- generation 0 has 71 finalizable objects (190ce568->190ce684) generation 1 has 32 finalizable objects (190ce4e8->190ce568) generation 2 has 56598 finalizable objects (19097090->190ce4e8) Finalizable but not rooted: 976186dc 97619774 9761981c 97619844 9da920c8 9da920e0 9da9213c 9da92150 …… Ready for finalization 0 objects (190ce684->190ce684) Statistics for all finalizable objects that are no longer rooted: MT Count TotalSize Class Name 6c3711b8 1 16 System.Threading.Gen2GcCallback 6c36b328 2 24 System.Threading.TimerHolder 6b68c0fc 1 24 System.Net.Sockets.TcpClient 6b671fec 1 40 System.Net.Sockets.NetworkStream 6b68105c 8 160 System.Net.SafeCloseSocket+InnerSafeCloseSocket 6b6811a8 8 192 System.Net.SafeCloseSocket 6c35e2cc 4 208 System.Threading.Thread 67b788b8 4 208 System.Windows.Forms.Control+ThreadMethodEntry 00d8a37c 4 336 **************.NetCommunicator.SocketConnectionInfo 6b690e80 4 400 System.Net.Sockets.AcceptOverlappedAsyncResult 6b680f2c 20 400 System.Net.SafeNativeOverlapped 6c362a18 21 420 Microsoft.Win32.SafeHandles.SafeWaitHandle 6b671564 20 480 System.Net.Sockets.OverlappedCache 6b674f28 9 828 System.Net.Sockets.Socket 6c36207c 107 1284 System.WeakReference 6b671800 99 9900 System.Net.Sockets.OverlappedAsyncResult Total 313 objects
從這個結果能夠看到只有9個對象是沒有根引用能夠直接回收的,這說明其餘的一萬多個socket都是有root引用而形成內存沒法釋放。根引用是什麼?在垃圾回收過程當中起到什麼做用?
應用程序的根包含線程堆棧上的靜態字段、局部變量、CPU 寄存器、GC 句柄和終結隊列。 每一個根或者引用託管堆中的對象,或者設置爲空。
換言之,內存中衆多的Socket對象就是被其餘的變量引用了而沒法釋放。如何進一步查找這些對象的根引用呢?這須要藉助!gcroot
指令。GCRoot 命令將檢查整個託管堆和句柄表以查找其餘對象內的句柄和堆棧上的句柄。 而後,在每一個堆棧中搜索對象的指針,同時還搜索終結器隊列。內存中有一萬多個socket的對象,不須要所有去檢查gcroot,只要看過一部分就會發現規律,在這些對象的gcroot的結果中有不少是相似的,最底層的引用關係是這樣的:
-> 029197f0 *******.RoadGate.TcpCommunicator.CameraTcpCommunictor -> 029199e4 *******.NetCommunicator.SocketConnectionInfoFactory -> 029199f0 System.Collections.Concurrent.ConcurrentDictionary`2[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]] -> 5e7c58f4 System.Collections.Concurrent.ConcurrentDictionary`2+Tables[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]] -> 80f55d48 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]][] -> 5e7afc40 System.Collections.Concurrent.ConcurrentDictionary`2+Node[[System.Net.Sockets.Socket, System],[*******.NetCommunicator.SocketConnectionInfo, *******.RoadGate.Communicator]] -> 02925630 System.Net.Sockets.Socket
如何解讀這個引用關係呢?能夠從下往上看,下層的對象是被上層的對象使用的。就當前這個對象來講,首先被ConcurrentDictionary的Node內部類型包裝,而且是ConcurrentDictionary內部的Node數組中的一員,該Node數組又被ConcurrentDictionary中的Tables內部類型再次封裝,Tables內部類的對象直屬於ConcurrentDictionary對象。該對象又是被SocketConnectionInfoFactory
類對象使用。SocketConnectionInfoFactory
是業務上定義的類型,若是要檢查源代碼,這個位置就是檢查的入口。
既然ConcurrentDictionary中存放了大量的該釋放而未被是否的對象,那麼這個對象有多大呢?用!objsize
來檢查一下。
0:000> !objsize 029199f0 sizeof(029199f0) = 1141780680 (0x440e30c8) bytes (System.Collections.Concurrent.ConcurrentDictionary`2[[System.Net.Sockets.Socket, System],[IOT.NetCommunicator.SocketConnectionInfo, IOT.RoadGate.Communicator]])
經過windbg的統計,這一個對象存放的內容就佔用了1G+的內存,跟抓取dump時的監控數據比較吻合。至此,內存泄漏的元兇就已經水落石出。
有了上一個章節內容的基礎,再從代碼角度分析出問題就比較容易了,代碼中確實使用了多個ConcurrentDictionary保存了socket對象和一些業務對象的映射關係,可是對於設備斷線重連的狀況處理並不完善,致使重連後部分ConcurrentDictionary的內容獲得了更新,而部分字典的內容並未被更新,並進而致使了內存泄漏的問題。
用僞代碼描述設備上線和離網過程當中的相關邏輯:
//設備上線 if(不容許上線) return; else 建立Socket對象socket1; if(字典1中存在設備特徵碼ID) 字典1[ID]=socket1; else 字典1.Add(ID,socket1); 字典2.Add(socket1,業務對象); //設備離線 if(字典1中存在設備特徵碼ID) 字典1.Remove(ID);
從僞代碼中很容易看出來因爲設備上線的時候往字典2中添加了內容可是設備離網以及設備建立重複鏈接的時候並無更新字典2中的內容致使了同一個設備會存在不少無用的socket對象。而這些對象沒有業務上的意義並且還由於具備root而沒法被清除。
內存泄漏問題是後臺服務中比較常見的一類故障,在發生內存泄漏事故時,若是單純從服務運行場景的角度來分析每每得不到太好的效果並且耗時長而且難以找到準確的故障點。藉助於windbg及sos插件的功能,綜合使用gcroot
、dumpheap
和finalizequeue
等指令快速定位內存泄漏的準確位置,並在此基礎上結合一些業務方面的知識和一些代碼上的分析,就能夠快速分析出內存泄漏的場景和緣由,並針對性的制定出相應的修復計劃。