背景
Netty中的ByteBuf是make things right的關鍵,對象本身可以被對象池回收,而它所占據(jù)的內存空間也可以被回收再分配,而這一切都是通過調用release來達成。
自從Netty 4開始,對象的生命周期由它們的引用計數(shù)管理,而不是由垃圾收集器管理了。Netty的原意是當引用計數(shù)歸零才需要去release, 由于JVM并沒有意識到Netty實現(xiàn)的引用計數(shù)對象,它仍會將這些引用計數(shù)對象當做常規(guī)對象處理,也就意味著,當不為0的引用計數(shù)對象變得不可達時仍然會被GC自動回收。一旦被GC回收,那么意味著該死的release我永遠都無法觸達,這樣便會造成內存泄露。舉個實際的經常犯的毛病, ByteBuf用完忘記release. 如果沒有一定的機制, 你可能永遠都發(fā)現(xiàn)不了.
當然, Netty的方案并沒有給社區(qū)提供包山包海通天的解決方案, 他是根據(jù)設定的頻率來檢測可能的泄漏, 最終通過日志告知開發(fā)者有泄露,要求開發(fā)者來排查問題。
引用
在深入Netty的解決方案前, 我們有必要先回顧下Java的幾種引用類型.
- 強引用,最普遍的引用,類似
Object obj = new Object()這類的引用。只要強引用還存在,垃圾回收器就不會回收掉被引用的對象。當內存空間不足,JVM寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足的問題。- 軟引用(
SoftReference類),如果一個對象只具有軟引用,則內存空間足夠,垃圾回收器就不會回收它,而如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現(xiàn)內存敏感的緩存。- 弱引用(
WeakReference類),弱引用與軟引用的區(qū)別在于:只具有弱引用的對象擁有更短暫的生命周期。垃圾回收器進行對象掃描時,一旦發(fā)現(xiàn)了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。- 虛/幻影引用(
PhantomReference類),虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。而且也無法通過虛引用來取得一個對象實例。當垃圾回收器準備回收一個對象時,如果發(fā)現(xiàn)它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯(lián)的引用隊列ReferenceQueue中。
假如
當我們的資源被GC時, Phantom Reference隊列能取到指向它的 PhantomReference. 前提是這個PhantomReference不能是孤立的, 不然會被GC掉. 解決辦法也很簡單粗暴, 我們需要提供一個容器來托管他們, 只要容器不倒, 他們就不會消失. 一旦該資源被成功release, 那么立即從這個容器中移除掉. 那么該資源的PhantomReference不久就會被GC掉.
泄露監(jiān)測
protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get();
PoolArena<byte[]> heapArena = cache.heapArena;
ByteBuf buf;
if (heapArena != null) {
buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
} else {
buf = new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
}
// 在新建ByteBuf的時候, 會開始監(jiān)控該buf是否會泄漏
return toLeakAwareBuffer(buf);
}
// 裝飾器模式, 對現(xiàn)有buf的增強
protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
ResourceLeak leak;
switch (ResourceLeakDetector.getLevel()) {
// 至于下面的level不是重點, 內存泄漏的監(jiān)控也是要成本的, 就看怎么取舍
// 而不同的level都會去到AbstractByteBuf.leakDetector.open
// 這里很形象,就是告訴leakDetector我要檢測這個對象, 如果發(fā)生泄漏上報給我.
case SIMPLE:
leak = AbstractByteBuf.leakDetector.open(buf);
if (leak != null) {
buf = new SimpleLeakAwareByteBuf(buf, leak);
}
break;
case ADVANCED:
case PARANOID:
leak = AbstractByteBuf.leakDetector.open(buf);
if (leak != null) {
buf = new AdvancedLeakAwareByteBuf(buf, leak);
}
break;
default:
break;
}
return buf;
}
public final ResourceLeak open(T obj) {
Level level = ResourceLeakDetector.level;
if (level == Level.DISABLED) {
return null;
}
if (level.ordinal() < Level.PARANOID.ordinal()) {
// 每隔128次泄漏檢查就要出具報告一次
if ((++ leakCheckCnt & mask) == 0) {
reportLeak(level);
return new DefaultResourceLeak(obj);
} else {
return null;
}
} else {
reportLeak(level);
return new DefaultResourceLeak(obj);
}
}
private void reportLeak(Level level) {
// 首先你的日志級別要是error, 否則將refQueue里面對象全部清掉
// 換句話說, 只要日志不對, 泄漏檢測就什么都不做
if (!logger.isErrorEnabled()) {
for (;;) {
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
ref.close();
}
return;
}
// 如果你申請監(jiān)控的資源對象太多也要提醒開發(fā)者.
int samplingInterval = level == Level.PARANOID? 1 : this.samplingInterval;
if (active * samplingInterval > maxActive && loggedTooManyActive.compareAndSet(false, true)) {
reportInstancesLeak(resourceType);
}
// 遍歷refQueue
for (;;) {
@SuppressWarnings("unchecked")
DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();
if (ref == null) {
break;
}
// 這里保證ref不會再回到refQueue里面
ref.clear();
// 這里是將DefaultResourceLeak從隊列中刪除, 也就是從觀察名單中移除
if (!ref.close()) {
continue;
}
// 接下來就是生成這個資源對象的泄露報告了
// 這里的records主要是該資源在每次retain的時候,視情況去記錄軌跡,說白了就是使用記錄
// 如果返回空,那么只上報基本情況,否則將軌跡一起上報.
// 里面很簡單, 就不再深入了
String records = ref.toString();
if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
if (records.isEmpty()) {
reportUntracedLeak(resourceType);
} else {
reportTracedLeak(resourceType, records);
}
}
}
}
容器
關鍵屬性
// 創(chuàng)建記錄
private final String creationRecord;
// 引用記錄軌跡
private final Deque<String> lastRecords = new ArrayDeque<String>();
// 是否被close
private final AtomicBoolean freed;
// 前驅
private DefaultResourceLeak prev;
// 后繼
private DefaultResourceLeak next;
// 從上面就可以看出來這個容器是以DefaultResourceLeak為節(jié)點類型的雙向鏈表
// 刪除的軌跡記錄
private int removedRecords;
DefaultResourceLeak(Object referent) {
// 包裝PhantomReference, 捎上refQueue, JVM垃圾回收時會將滿足條件的填入queue中
super(referent, referent != null? refQueue : null);
if (referent != null) {
Level level = getLevel();
if (level.ordinal() >= Level.ADVANCED.ordinal()) {
creationRecord = newRecord(null, 3);
} else {
creationRecord = null;
}
// 將該兼容資源假如到這個雙向列表中.
synchronized (head) {
prev = head;
next = head.next;
head.next.prev = this;
head.next = this;
active ++;
}
freed = new AtomicBoolean();
} else {
creationRecord = null;
freed = new AtomicBoolean(true);
}
}