Netty本身在內存分配上支持堆內存和直接內存,我們一般選用直接內存,這也是默認的配置。所以要理解Netty內存的釋放我們得先看下直接內存的釋放。
Java直接內存釋放
我們先來看下直接內存是怎么使用的
ByteBuffer.allocateDirect(capacity)
申請的過程是其實就是創建一個DirectByteBuffer對象的過程,DirectByteBuffer對象只相當於一個holder,包含一個address,這個是直接內存的指針。
- 調用native方法申請內存
- 初始化cleaner
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) { // package-private
// 省略中間代碼...
// 創建一個cleaner,最后會調用Deallocator.run來釋放內存
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
Cleaner這個類繼承自PhantomReference,也就是所謂的虛引用,這種類型引用的特點是:
- 使用get方法不能獲取到對象
- 只要引用的對象除了PhantomReference之外沒有其他引用了,JVM隨時可以將PhantomReference引用的對象回收。
JVM在回前會將將要被回收的對象放在一個隊列中,由於Cleaner繼承自PhantomReference,隊列的實現是使用cleaner的
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue<>();
這個隊列在PhantomReference的父類Reference中使用到了,Reference這個類在初始化的時候會啟動一個線程來調用cleaner.clean方法,在Reference的靜態代碼塊中啟動線程
// java.lang.ref.Reference
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
// 啟動ReferenceHandler線程
handler.start();
// 省略中間代碼...
}
該線程的主要作用就是調用tryHandlePending
// java.lang.ref.Reference#tryHandlePending
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Fast path for cleaners
if (c != null) {
// 調用clean方法
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
System.gc不能回收堆外內存,但是會回收已經沒有使用了DirectByteBuffer對象,該對象被回收的時候會將cleaner對象放入隊列中,在Reference的線程中調用clean方法來回收堆外內存 。cleaner.run執行的是傳入參數的thunk.run方法,這里thunk是Deallocator,所以最后執行的Deallocator.run方法
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 釋放內存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
所以最后通過unsafe.freeMemory釋放了申請到的內存。
總結一下,在申請內存的時候調用的是java.nio.ByteBuffer#allocateDirect
會new DirectByteBuffer,通過Cleaner.create創建Cleaner,同時傳入Deallocator作為Runnable參數,在Cleaner.clean的時候會調用該Deallocator.run來處理
Cleaner繼承自PhantomReference,包含一個ReferenceQueue,在DirectByteBuffer不再使用的時候,該對象是處於Java堆的,除了該PhantomReference引用了DirectByteBuffer外,沒有其他引用的時候,jvm會把cleaner對象放入ReferenceQueue隊列中。
PhantomReference繼承了Reference,Reference會啟動一個線程(java.lang.ref.Reference.ReferenceHandler#run)去調用隊列中的cleaner.clean方法。
Netty內存釋放
Netty使用的直接內存的釋放方式和JDK的釋放方式略有不同。Netty開始釋放內存的時候是調用free方法的時候
io.netty.buffer.PoolArena#free
io.netty.buffer.PoolArena.DirectArena#destroyChunk
最終釋放內存的方法有兩種
- 利用反射獲取unsafe,調用Unsafe#freeMemory
- 利用反射獲取DirectByteBuffer#cleaner,通過反射調用cleaner.clean方法
兩種不同的方式依賴的條件不同,使用場景也不同
使用反射調用cleaner.clean
要滿足以下條件之一的時候使用這種方式
- 沒有可使用的直接內存
- 不能獲取unsafe
- directBuffer沒有傳入long、int的構造方法
使用unsafe
不能使用上面這種方式的都使用unsafe