Netty4原始碼分析之記憶體管理
阿新 • • 發佈:2019-02-05
引用計數
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使用裝飾器模式,為ByteBuf增加記憶體跟蹤記錄功能.利用虛引用跟蹤資源被VM垃圾回收的情況,加上ByteBuf的引用計數特性,進而判斷是否發生記憶體洩露.
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的方法呼叫堆疊,區別在於後者比前者記錄更多的內容 protected static 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) {//禁用記憶體跟蹤 return null; } if (level.ordinal() < Level.PARANOID.ordinal()) { //如果監控級別低於PARANOID,在一定的取樣頻率下報告記憶體洩露 if (leakCheckCnt ++ % samplingInterval == 0) { reportLeak(level); return new DefaultResourceLeak(obj); } else { return null; } } else { //每次需要分配 ByteBuf 時,報告記憶體洩露情況 reportLeak(level); return new DefaultResourceLeak(obj); } }
其中reportLeak方法中完成對記憶體洩露的檢測和報告,如下面程式碼所示.
private void 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);
}
}
}
}
綜合上面的三段程式碼,可以看出, Netty 在分配新 ByteBuf 時進行記憶體洩露檢測和報告.DefaultResourceLeak的宣告如下
private final class 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的引用計數特性,進而判斷是否發生記憶體洩露.