.Net調試內存泄漏不斷增長小記——SocketAsyncEventArgs

現象

用C#異步方式實現的網絡底層協議,開發的服務器。上線運行一段時間後,發現一開始內存非常穩定,但是過了一定時間後,內存使用量會開始不停的上漲。直到內存耗盡。

排查

遇到這一問題可以明確的是內存發生了泄漏。由於.Net中,託管對象的內存是由垃圾回收機制負責回收的。所以存在內存增長的情況,往往不是因爲沒有釋放。而是有幾種原因

  1. 分配的內存,比垃圾回收的還要快
  2. 對象存在引用,沒有辦法被垃圾回收機制回收。

對於第一種情況,我仔細排查了代碼中使用new分配內存的位置,往往這種情況,是因爲在一個邏輯循環中不斷分配內存,並且該循環佔用了大量的CPU,導致其他線程(GC)沒有空隙執行釋放造成的。常見的是在一些線程函數中的,無限循環(業務邏輯的幀循環)中執行了new。

第二種情況則是我們以爲我們分配的對象在生命週期結束後,不會再被其他對象引用,而應該被GC回收,但實際情況是因爲某些原因(比如,函數返回,輸出參數,將對應的引用返回到其生命週期外部),對象實際被其他對象使用了,導致GC不認爲該對象是死對象,從而無法回收。這種情況比較多見,往往是編寫代碼的疏忽。

但針對以上兩種情況進行排查後,我發現我們的代碼都沒有這種問題,因爲對這個問題的排查陷入一個無解的情況。

由於在進程中,執行new的地方,除了業務邏輯,只剩下網絡底層協議。因爲把目光轉向了網絡底層實現。在我們的網絡底層實現中,使用new的只有對網絡消息的封包,和網絡異步操作時的SocketAsyncEventArgs的使用。根據內存增長的速度,我們得出大概每秒會上漲接近200KB的內存,而我們測試時的峯值人數才100人,也就是平均每人每秒,在基本和網絡只保持心跳的情況下,都要發出去2000字節,而我們的心跳包大概也就2字節。所以基本排除了是消息封包的內存出現問題。

那麼接下來就是SocketAsyncEventArgs的分配了,仔細檢查代碼,這個東西我們是作爲局部變量使用的,每次都是重新分配,理應不會產生上述兩種情況。所以看似又進入了死衚衕。抱着試試看的態度,去網上查了下,看到.net 中異步SOCKET發送數據時碰到的內存問題這篇文章(這裏是我轉載的),發現裏面的問題和我們的非常類似,然後抱着試試的想法,對SocketAsyncEventArgs對象執行了SetBuffer(null, 0, 0),調試,問題解決。

原因

問題解決後,發現原來產生問題的原因是這樣的,按.net 中異步SOCKET發送數據時碰到的內存問題中的實驗來看,是因爲,沒有執行過SetBuffer(null, 0, 0)的話,先前在我們進行異步收發的時候SetBuffer()有設置過一個byte[]對象,而導致SocketAsyncEventArgs一直引用這個byte[],使byte[]無法釋放,產生內存增長。文章中最後總結爲兩個死對象互相引用,從而導致GC無法確定該釋放哪個。從而使兩個對象都無法被回收。

雖然我按他的方法試驗後,確實解決了問題。但我其實對這個原因還是存在一定的疑問。首先按照GC的回收機制,只有當從root對象中無法到達的對象纔會被認定爲死對象。那麼在這個問題中,由於沒有執行SetBuffer(null, 0, 0),那麼byte[]始終被SocketAsyncEventArgs引用,那麼如果SocketAsyncEventArgs是可達的話,byte[]纔會可達,而SocketAsyncEventArgs作爲局部變量,理應在函數執行完後,成爲一個死對象,連帶其中的byte[]都成爲不可達的,而byte[]並不可能引用SocketAsyncEventArgs,也就是不存在兩個死對象互相引用的問題。這也是我一直在開始並沒有懷疑這個new分配的原因。

其實真正的原因並不是兩個死對象互相引用,而是在於非託管資源與託管資源的釋放不同。SocketAsyncEventArgs具有Dispose接口,加之其和網絡相關,因此其內部很可能存在非託管的資源,因此不調用Dispose的話是無法釋放的。而byte[]如果被SocketAsyncEventArgs引用,而SocketAsyncEventArgs沒有釋放,那麼byte[]自然也無法釋放了。

在轉載文章的代碼中,將SetBuffer(null,0,0)修改成Dispose調用,同樣解決問題

class Program
{
   public class simpleRef
   {
       public byte[] byteref;

       public void SetBuffer(byte[] buf)
       {
           byteref = buf;
       }
   }

   static void Main(string[] args)
   {
       Console.WriteLine("Press any key to start.");
       Console.ReadKey();

       for (int i = 0; i < 100; ++i)
       {
           for(int j = 0; j < 10000; ++j)
           {
               var e = new SocketAsyncEventArgs();
               //e.SetBuffer(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }, 0, 0);
               //e.SetBuffer(null, 0, 0);
               //e.Dispose();
           }
           System.Threading.Thread.Sleep(1);
       }
       //}
       Console.WriteLine("Over!!!!!!!!!!!");
       Console.ReadLine();
   }
}

依次打開代碼中註釋的三行代碼測試,當不做任何處理時,內存峯值穩定在715752kb
使用轉載文章的方法,SetBuffer(null, 0, 0),內存峯值穩定在13120kb
使用Dispose,內存峯值穩定在7980kb

可以看到,在循環中,我們分配的byte,即便完全沒有釋放,也只有10000kb左右,由此可見,真正佔內存的資源是SocketAsyncEventArgs。SetBuffer這個函數會產生大量的內存消耗,其中應該包括大量非託管的資源。雖然SetBuffer(null, 0, 0)執行後,確實能釋放掉一部分資源,但顯然沒有釋放乾淨,只有使用Dispose,纔是正確的釋放方式

經過這個問題,老實說也提醒我平時要注意對於託管資源和非託管資源的釋放,平時太依賴GC,幾乎不太注意這個問題,看來也不是好習慣,敲響了警鐘

發佈了33 篇原創文章 · 獲贊 31 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章