Netty記憶體洩露檢測原理分析
阿新 • • 發佈:2019-02-09
引用計數
netty中使用引用計數機制來管理資源,當一個實現ReferenceCounted的物件例項化時,引用計數置1.
客戶程式碼中需要保持一個該物件的引用時需要呼叫介面的retain方法將計數增1.物件使用完畢時呼叫release將計數減1.
當引用計數變為0時,物件將釋放所持有的底層資源或將資源返回資源池.
記憶體洩露
按上述規則使用Direct和Pooled的ByteBuf尤其重要.對於DirectBuf,其記憶體不受VM垃圾回收控制只有在呼叫release導致計數為0時才會主動釋放記憶體,而PooledByteBuf只有在release後才能被回收到池中以迴圈利用.
如果客戶程式碼沒有按引用計數規則使用這兩種物件,將會導致記憶體洩露.
記憶體使用跟蹤
在netty.io.util包中含有如下兩個類
ResourceLeak 用於跟蹤記憶體洩露
ResourceLeakDetector 記憶體洩露檢測工具
可見DefaultResourceLeak是個”虛”引用型別,有別於常見的普通的”強”引用,虛引用完全不影響目標物件的垃圾回收,但是會在目標物件被VM垃圾回收時被加入到引用佇列中.
在正常情況下ResourceLeak物件會所監控的資源的引用計數為0時被清理掉(不在被加入引用佇列),所以一旦資源的引用計數失常,ResourceLeak物件會被加入到引用佇列.例如沒有成對呼叫ByteBuf的retain和relaease方法,導致ByteBuf沒有被正常釋放(對於DirectByteBuf沒有及時釋放記憶體,對於PooledByteBuf沒有返回Pool),當引用佇列中存在元素時意味著程式中有記憶體洩露發生.
ResourceLeakDetector通過檢查引用佇列來判斷是否有記憶體洩露,並報告跟蹤情況.
總結
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類中有如下程式碼
- //裝飾器模式,用SimpleLeakAwareByteBuf或AdvancedLeakAwareByteBuf來包裝原始的ByteBuf
- //兩個包裝類均通過呼叫ResourceLeak的record方法來記錄ByteBuf的方法呼叫堆疊,區別在於後者比前者記錄更多的內容
-
protectedstatic ByteBuf toLeakAwareBuffer(ByteBuf buf) {
- ResourceLeak leak;
- //根據設定的Level來選擇使用何種包裝器
- switch (ResourceLeakDetector.getLevel()) {
- case SIMPLE:
- //建立用於跟蹤和表示內容洩露的ResourcLeak物件
- leak = AbstractByteBuf.leakDetector.open(buf);
- if (leak != null) {
-
//只在ByteBuf.order方法中呼叫ResourceLeak.record
- buf = new SimpleLeakAwareByteBuf(buf, leak);
- }
- break;
- case ADVANCED:
- case PARANOID:
- leak = AbstractByteBuf.leakDetector.open(buf);
- if (leak != null) {
- //在ByteBuf幾乎所有方法中呼叫ResourceLeak.record
- buf = new AdvancedLeakAwareByteBuf(buf, leak);
- }
- break;
- }
- return buf;
- }
下圖展示了該方法被呼叫的時機.可見Netty只對PooledByteBuf和DirectByteBuf監控記憶體洩露.
記憶體洩露檢測
下面觀察上述程式碼中的AbstractByteBuf.leakDetector.open(buf);
實現程式碼如下- //建立用於跟蹤和表示內容洩露的ResourcLeak物件
- public ResourceLeak open(T obj) {
- Level level = ResourceLeakDetector.level;
- if (level == Level.DISABLED) {//禁用記憶體跟蹤
- returnnull;
- }
- if (level.ordinal() < Level.PARANOID.ordinal()) {
- //如果監控級別低於PARANOID,在一定的取樣頻率下報告記憶體洩露
- if (leakCheckCnt ++ % samplingInterval == 0) {
- reportLeak(level);
- returnnew DefaultResourceLeak(obj);
- } else {
- returnnull;
- }
- } else {
- //每次需要分配 ByteBuf 時,報告記憶體洩露情況
- reportLeak(level);
- returnnew DefaultResourceLeak(obj);
- }
- }
其中reportLeak方法中完成對記憶體洩露的檢測和報告,如下面程式碼所示.
- privatevoid reportLeak(Level level) {
- //......
- // 報告生成了太多的活躍資源
- int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
- if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
- logger.error("LEAK: You are creating too many " + resourceType + " instances. " +
- resourceType + " is a shared resource that must be reused across the JVM," +
- "so that only a few instances are created.");
- }
- // 檢測並報告之前發生的記憶體洩露
- for (;;) {
- @SuppressWarnings("unchecked")
- //檢查引用佇列(為什麼通過檢查該佇列,可以判斷是否存在記憶體洩露)
- DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
- if (ref == null) {//佇列為空,沒有未報告的記憶體洩露或者從未發生記憶體洩露
- break;
- }
- <span style="white-space:pre"> </span>//清理引用
- ref.clear();
- if (!ref.close()) {
- continue;
- }
- //通過錯誤日誌列印資源的方法呼叫記錄,並將其儲存在reportedLeaks中
- String records = ref.toString();
- if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
- if (records.isEmpty()) {
- logger.error("LEAK: {}.release() was not called before it's garbage-collected. " +
- "Enable advanced leak reporting to find out where the leak occurred. " +
- "To enable advanced leak reporting, " +
- "specify the JVM option '-D{}={}' or call {}.setLevel()",
- resourceType, PROP_LEVEL, Level.ADVANCED.name().toLowerCase(), simpleClassName(this));
- } else {
- logger.error(
- "LEAK: {}.release() was not called before it's garbage-collected.{}",
- resourceType, records);
- }
- }
- }
- }
DefaultResourceLeak的宣告如下
- privatefinalclass DefaultResourceLeak extends PhantomReference<Object> implements ResourceLeak{
- //......
- public DefaultResourceLeak(Object referent) {
- //使用一個靜態的引用佇列(refQueue)初始化
- //refQueue是ResourceLeakDecetor的成員變數並由其初始化
- super(referent, referent != null? refQueue : null);
- //......
- }
- //......
- }
可見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