1. 程式人生 > >Netty記憶體洩露檢測原理分析

Netty記憶體洩露檢測原理分析

引用計數
netty中使用引用計數機制來管理資源,當一個實現ReferenceCounted的物件例項化時,引用計數置1.
客戶程式碼中需要保持一個該物件的引用時需要呼叫介面的retain方法將計數增1.物件使用完畢時呼叫release將計數減1.
當引用計數變為0時,物件將釋放所持有的底層資源或將資源返回資源池.

記憶體洩露
按上述規則使用Direct和Pooled的ByteBuf尤其重要.對於DirectBuf,其記憶體不受VM垃圾回收控制只有在呼叫release導致計數為0時才會主動釋放記憶體,而PooledByteBuf只有在release後才能被回收到池中以迴圈利用.
如果客戶程式碼沒有按引用計數規則使用這兩種物件,將會導致記憶體洩露.


記憶體使用跟蹤
在netty.io.util包中含有如下兩個類
ResourceLeak 用於跟蹤記憶體洩露
ResourceLeakDetector 記憶體洩露檢測工具

在io.netty.buffer.AbstractByteBufAllocator類中有如下程式碼

  1. //裝飾器模式,用SimpleLeakAwareByteBuf或AdvancedLeakAwareByteBuf來包裝原始的ByteBuf
  2. //兩個包裝類均通過呼叫ResourceLeak的record方法來記錄ByteBuf的方法呼叫堆疊,區別在於後者比前者記錄更多的內容
  3. protectedstatic ByteBuf toLeakAwareBuffer(ByteBuf buf) {  
  4.     ResourceLeak leak;  
  5.     //根據設定的Level來選擇使用何種包裝器
  6.     switch (ResourceLeakDetector.getLevel()) {  
  7.         case SIMPLE:  
  8.             //建立用於跟蹤和表示內容洩露的ResourcLeak物件
  9.             leak = AbstractByteBuf.leakDetector.open(buf);  
  10.             if (leak != null) {  
  11.                 //只在ByteBuf.order方法中呼叫ResourceLeak.record
  12.                 buf = new SimpleLeakAwareByteBuf(buf, leak);  
  13.             }  
  14.             break;  
  15.         case ADVANCED:  
  16.         case PARANOID:  
  17.             leak = AbstractByteBuf.leakDetector.open(buf);  
  18.             if (leak != null) {  
  19.                 //在ByteBuf幾乎所有方法中呼叫ResourceLeak.record  
  20.                 buf = new AdvancedLeakAwareByteBuf(buf, leak);  
  21.             }  
  22.             break;  
  23.     }  
  24.     return buf;  
  25. }  

下圖展示了該方法被呼叫的時機.可見Netty只對PooledByteBuf和DirectByteBuf監控記憶體洩露.



記憶體洩露檢測

下面觀察上述程式碼中的AbstractByteBuf.leakDetector.open(buf);

實現程式碼如下
  1. //建立用於跟蹤和表示內容洩露的ResourcLeak物件
  2. public ResourceLeak open(T obj) {  
  3.     Level level = ResourceLeakDetector.level;  
  4.     if (level == Level.DISABLED) {//禁用記憶體跟蹤
  5.         returnnull;  
  6.     }  
  7.     if (level.ordinal() < Level.PARANOID.ordinal()) {  
  8.         //如果監控級別低於PARANOID,在一定的取樣頻率下報告記憶體洩露
  9.         if (leakCheckCnt ++ % samplingInterval == 0) {  
  10.             reportLeak(level);  
  11.             returnnew DefaultResourceLeak(obj);  
  12.         } else {  
  13.             returnnull;  
  14.         }  
  15.     } else {  
  16.         //每次需要分配 ByteBuf 時,報告記憶體洩露情況
  17.         reportLeak(level);  
  18.         returnnew DefaultResourceLeak(obj);  
  19.     }  
  20. }  

其中reportLeak方法中完成對記憶體洩露的檢測和報告,如下面程式碼所示.

  1. privatevoid reportLeak(Level level) {  
  2.     //......
  3.     // 報告生成了太多的活躍資源
  4.     int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;  
  5.     if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(falsetrue)) {  
  6.         logger.error("LEAK: You are creating too many " + resourceType + " instances.  " +  
  7.                 resourceType + " is a shared resource that must be reused across the JVM," +  
  8.                 "so that only a few instances are created.");  
  9.     }  
  10.     // 檢測並報告之前發生的記憶體洩露
  11.     for (;;) {  
  12.         @SuppressWarnings("unchecked")  
  13.         //檢查引用佇列(為什麼通過檢查該佇列,可以判斷是否存在記憶體洩露)
  14.         DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();  
  15.         if (ref == null) {//佇列為空,沒有未報告的記憶體洩露或者從未發生記憶體洩露
  16.             break;  
  17.         }  
  18. <span style="white-space:pre">    </span>//清理引用
  19.         ref.clear();  
  20.         if (!ref.close()) {  
  21.             continue;  
  22.         }  
  23.         //通過錯誤日誌列印資源的方法呼叫記錄,並將其儲存在reportedLeaks中
  24.         String records = ref.toString();  
  25.         if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {  
  26.             if (records.isEmpty()) {  
  27.                 logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +  
  28.                         "Enable advanced leak reporting to find out where the leak occurred. " +  
  29.                         "To enable advanced leak reporting, " +  
  30.                         "specify the JVM option '-D{}={}' or call {}.setLevel()",  
  31.                         resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));  
  32.             } else {  
  33.                 logger.error(  
  34.                         "LEAK: {}.release() was not called before it's garbage-collected.{}",  
  35.                         resourceType, records);  
  36.             }  
  37.         }  
  38.     }  
  39. }  
綜合上面的三段程式碼,可以看出, Netty 在分配新 ByteBuf 時進行記憶體洩露檢測和報告.

DefaultResourceLeak的宣告如下

  1. privatefinalclass DefaultResourceLeak extends PhantomReference<Object> implements ResourceLeak{  
  2.     //......
  3.     public DefaultResourceLeak(Object referent) {  
  4.         //使用一個靜態的引用佇列(refQueue)初始化
  5.         //refQueue是ResourceLeakDecetor的成員變數並由其初始化
  6.         super(referent, referent != null? refQueue : null);  
  7.         //......
  8.     }  
  9.     //......
  10. }  

可見DefaultResourceLeak是個”虛”引用型別,有別於常見的普通的”強”引用,虛引用完全不影響目標物件的垃圾回收,但是會在目標物件被VM垃圾回收時被加入到引用佇列中.
在正常情況下ResourceLeak物件會所監控的資源的引用計數為0時被清理掉(不在被加入引用佇列),所以一旦資源的引用計數失常,ResourceLeak物件會被加入到引用佇列.例如沒有成對呼叫ByteBuf的retain和relaease方法,導致ByteBuf沒有被正常釋放(對於DirectByteBuf沒有及時釋放記憶體,對於PooledByteBuf沒有返回Pool),當引用佇列中存在元素時意味著程式中有記憶體洩露發生.
ResourceLeakDetector通過檢查引用佇列來判斷是否有記憶體洩露,並報告跟蹤情況.

總結

Netty使用裝飾器模式,為ByteBuf增加記憶體跟蹤記錄功能.利用虛引用跟蹤資源被VM垃圾回收的情況,加上ByteBuf的引用計數特性,進而判斷是否發生記憶體洩露.

本文摘自:http://blog.csdn.net/hadixlin/article/details/19301377