概述
之前寫過篇文章,關于堆外內存的,JVM源碼分析之堆外內存完全解讀,里面重點講了DirectByteBuffer的原理,但是今天碰到一個比較奇怪的問題,在設置了-XX:MaxDirectMemorySize=1G的前提下,然后統計所有DirectByteBuffer對象后面占用的內存達到了7G,遠遠超出閾值,這個問題很詭異,于是好好查了下原因,雖然最終發現是我們統計的問題,但是期間發現的其他一些問題還是值得分享一下的。
不得不提的DirectByteBuffer構造函數
打開DirectByteBuffer這個類,我們會發現有5個構造函數
DirectByteBuffer(int cap);
DirectByteBuffer(long addr, int cap, Object ob);
private DirectByteBuffer(long addr, int cap);
protected DirectByteBuffer(int cap, long addr,FileDescriptor fd,Runnable unmapper);
DirectByteBuffer(DirectBuffer db, int mark, int pos, int lim, int cap,int off)
我們從java層面創建DirectByteBuffer對象,一般都是通過ByteBuffer的allocateDirect方法
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
也就是會使用上面提到的第一個構造函數,即
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.create(this, new Deallocator(base, size, cap));
att = null;
}
而這個構造函數里的Bits.reserveMemory(size, cap)
方法會做堆外內存的閾值check
static void reserveMemory(long size, int cap) {
synchronized (Bits.class) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// -XX:MaxDirectMemorySize limits the total capacity rather than the
// actual memory usage, which will differ when buffers are page
// aligned.
if (cap <= maxMemory - totalCapacity) {
reservedMemory += size;
totalCapacity += cap;
count++;
return;
}
}
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException x) {
// Restore interrupt status
Thread.currentThread().interrupt();
}
synchronized (Bits.class) {
if (totalCapacity + cap > maxMemory)
throw new OutOfMemoryError("Direct buffer memory");
reservedMemory += size;
totalCapacity += cap;
count++;
}
}
因此當我們已經分配的內存超過閾值的時候會觸發一次gc動作,并重新做一次分配,如果還是超過閾值,那將會拋出OOM,因此分配動作會失敗。
所以從這一切看來,只要設置了-XX:MaxDirectMemorySize=1G
是不會出現超過這個閾值的情況的,會看到不斷的做GC。
構造函數再探
那其他的構造函數主要是用在什么情況下的呢?
我們知道DirectByteBuffer回收靠的是里面有個cleaner的屬性,但是我們發現有幾個構造函數里cleaner這個屬性卻是null,那這種情況下他們怎么被回收呢?
那下面請大家先看下DirectByteBuffer里的這兩個函數:
public ByteBuffer slice() {
int pos = this.position();
int lim = this.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
int off = (pos << 0);
assert (off >= 0);
return new DirectByteBuffer(this, -1, 0, rem, rem, off);
}
public ByteBuffer duplicate() {
return new DirectByteBuffer(this,
this.markValue(),
this.position(),
this.limit(),
this.capacity(),
0);
}
從名字和實現上基本都能猜出是干什么的了,slice其實是從一塊已知的內存里取出剩下的一部分,用一個新的DirectByteBuffer對象指向它,而duplicate就是創建一個現有DirectByteBuffer的全新副本,各種指針都一樣。
因此從這個實現來看,后面關聯的堆外內存其實是同一塊,所以如果我們做統計的時候如果僅僅將所有DirectByteBuffer對象的capacity加起來,那可能會導致算出來的結果偏大不少,這其實也是我查的那個問題,本來設置了閾值1G,但是發現達到了7G的效果。所以這種情況下使用的構造函數,可以讓cleaner為null,回收靠原來的那個DirectByteBuffer對象被回收。
被遺忘的檢查
但是還有種情況,也是本文要講的重點,在jvm里可以通過jni方法回調上面的DirectByteBuffer構造函數,這個構造函數是
private DirectByteBuffer(long addr, int cap) {
super(-1, 0, cap, cap);
address = addr;
cleaner = null;
att = null;
}
而調用這個構造函數的jni方法是jni_NewDirectByteBuffer
extern "C" jobject JNICALL jni_NewDirectByteBuffer(JNIEnv *env, void* address, jlong capacity)
{
// thread_from_jni_environment() will block if VM is gone.
JavaThread* thread = JavaThread::thread_from_jni_environment(env);
JNIWrapper("jni_NewDirectByteBuffer");
#ifndef USDT2
DTRACE_PROBE3(hotspot_jni, NewDirectByteBuffer__entry, env, address, capacity);
#else /* USDT2 */
HOTSPOT_JNI_NEWDIRECTBYTEBUFFER_ENTRY(
env, address, capacity);
#endif /* USDT2 */
if (!directBufferSupportInitializeEnded) {
if (!initializeDirectBufferSupport(env, thread)) {
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, NewDirectByteBuffer__return, NULL);
#else /* USDT2 */
HOTSPOT_JNI_NEWDIRECTBYTEBUFFER_RETURN(
NULL);
#endif /* USDT2 */
return NULL;
}
}
// Being paranoid about accidental sign extension on address
jlong addr = (jlong) ((uintptr_t) address);
// NOTE that package-private DirectByteBuffer constructor currently
// takes int capacity
jint cap = (jint) capacity;
jobject ret = env->NewObject(directByteBufferClass, directByteBufferConstructor, addr, cap);
#ifndef USDT2
DTRACE_PROBE1(hotspot_jni, NewDirectByteBuffer__return, ret);
#else /* USDT2 */
HOTSPOT_JNI_NEWDIRECTBYTEBUFFER_RETURN(
ret);
#endif /* USDT2 */
return ret;
}
想象這么種情況,我們寫了一個native方法,里面分配了一塊內存,同時通過上面這個方法和一個DirectByteBuffer對象關聯起來,那從java層面來看這個DirectByteBuffer確實是一個有效的占有不少native內存的對象,但是這個對象后面關聯的內存完全繞過了MaxDirectMemorySize的check,所以也可能給你造成這種現象,明明設置了MaxDirectMemorySize,但是發現DirectByteBuffer關聯的堆外內存其實是大于它的。