前言
對於堆外內存,使用 System.gc() 是不靠譜的,依賴老年代 FGC 也是不靠譜的,而且大部分調優指南都設置了 -DisableExplicitGC 禁用 System.gc()。所以主動回收比較靠譜, JDK 在 DirectByteBuffer 中提供了 Cleaner 用來主動釋放內存。同時還有 Unsafe 的 freeMemory 方法也可以。 下面看看他是怎么做的。這里以非池化創建直接內存為例。
UnpooledByteBufAllocator newDirectBuffer 方法
代碼如下:
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
final ByteBuf buf;
if (PlatformDependent.hasUnsafe()) {
buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
} else {
buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
}
關鍵點在於 noCleaner 的結果。影響其結果的代碼如下:
// 構造方法,
public UnpooledByteBufAllocator(boolean preferDirect, boolean disableLeakDetector, boolean tryNoCleaner) {
super(preferDirect);
this.disableLeakDetector = disableLeakDetector;
noCleaner = tryNoCleaner && PlatformDependent.hasUnsafe()
&& PlatformDependent.hasDirectBufferNoCleanerConstructor();
}
// tryNoCleaner 結果來自 PlatformDependent.useDirectBufferNoCleaner()
public UnpooledByteBufAllocator(boolean preferDirect, boolean disableLeakDetector) {
this(preferDirect, disableLeakDetector, PlatformDependent.useDirectBufferNoCleaner());
}
// 判斷是否含有 DirectByteBuffer 構造器,有則 true
public static boolean useDirectBufferNoCleaner() {
return USE_DIRECT_BUFFER_NO_CLEANER;
}
// 根據是否含有 DirectByteBuffer 的構造器判斷,如果沒有,USE_DIRECT_BUFFER_NO_CLEANER=false
if (maxDirectMemory == 0 || !hasUnsafe() || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) {
USE_DIRECT_BUFFER_NO_CLEANER = false;
DIRECT_MEMORY_COUNTER = null;
} else {
USE_DIRECT_BUFFER_NO_CLEANER = true;
if (maxDirectMemory < 0) {
maxDirectMemory = maxDirectMemory0();
if (maxDirectMemory <= 0) {
DIRECT_MEMORY_COUNTER = null;
} else {
DIRECT_MEMORY_COUNTER = new AtomicLong();
}
} else {
DIRECT_MEMORY_COUNTER = new AtomicLong();
}
}
// 獲取 DirectByteBuffer 的構造器
final ByteBuffer direct;
Constructor<?> directBufferConstructor;
long address = -1;
try {
final Object maybeDirectBufferConstructor =
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
try {
final Constructor<?> constructor =
direct.getClass().getDeclaredConstructor(long.class, int.class);
Throwable cause = ReflectionUtil.trySetAccessible(constructor, true);
if (cause != null) {
return cause;
}
return constructor;
} catch (NoSuchMethodException e) {
return e;
} catch (SecurityException e) {
return e;
}
}
});
((Constructor<?>) maybeDirectBufferConstructor).newInstance(address, 1);
directBufferConstructor = (Constructor<?>) maybeDirectBufferConstructor;
} finally {
if (address != -1) {
UNSAFE.freeMemory(address);
}
}
DIRECT_BUFFER_CONSTRUCTOR = directBufferConstructor;
noCleaner 為 true:創建 InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf 對象。簡稱 noCleaner;
noCleaner 為false:創建 InstrumentedUnpooledUnsafeDirectByteBuf 對象。簡稱 hasCleaner;
兩者構造器方式不同:
noCleaner 反射調用 private DirectByteBuffer(long addr, int cap)
hasCleaner new 操作調用 DirectByteBuffer(int cap)
兩個釋放內存方式不同:
noCleaner 使用 unSafe.freeMemory(address);
hasCleaner 使用 DirectByteBuffer 的 Cleaner 的 clean 方法。
hasCleaner 的 clean 方法有 2 種策略:
1.Java9 使用 Unsafe 的 invokeCleaner 方法。調用了 ByteBuffer 的 Cleaner 的 clean 方法。
2. Java6 --- Java9 使用 DirectByteBuffer 的 屬性 Cleaner 的 clean 方法。
clean 方法原理:
這個 clean 方法內部調用了一個名為 thunk 的 Deallocator 線程的 run 方法。該線程對象在創建 DirectByteBuffer 的時候同時創建。該線程的 run 方法內部會調用 unsafe 的 freeMemory 方法,同時還會調用 Bits.unreserveMemory 方法,該方法會相應的減小已經使用的內存大小數字(因為,每次申請直接內存都需要 Bits 判斷是否足夠,如果 FGC 后還不夠,OOM,所以,這里的做法還是挺重要的)
注意:這個 Cleaner 是個虛引用,DirectByteBuffer 創建他的時候,會將自己放入虛引用的構造函數中,如果這個 DirectByteBuffer 被回收了(無人再引用這個 Cleaner),那么 GC 將會把這個 Cleaner 賦值給 Reference 的 pending 變量中,專門有一條 ReferenceHandler 的線程會死循環執行 Reference 的 tryHandlePending 方法,這個方法會調用 pending 的 clean 方法,完成內存回收操作。
這是 cleaner 對象的構造時機:
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 這里構造 cleaner
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
該構造只有一個 int 參數
所以,你知道了吧,noCleaner 的構造方法是不能調用 cleaner 的 clean 方法的。只能使用 unSafe 的 freeMemory 方法。而這就是 Netty 默認的做法。
同時,noCleaner 的構造方法也沒有向 Bits 申請內存的內容,在申請內存的時候,性能會比 hasCleaner 要好一點。關於 Bits 的設計,我覺得不夠優雅。當內存不夠了,就 System.gc(),卻只休眠 100 毫秒。根本不夠回收到堆外內存。
實際上,Cleaner 的作用除了更新一下 Bits 的一些屬性,方便下次申請內存之外,別無作用。
我猜想 Netty 使用 noCleaner 是性能優化的考慮吧。為了防止用戶忘記使用 ReferenceCountUtil.release(), 導致內存泄漏,Netty 還使用了虛引用跟蹤每一個 ByteBuf,基本上避免了內存泄漏的發生。
綜上所述:noCleaner 無論是在申請內存還是釋放內存都比使用 hasCleaner 性能好要好一點。