JVM源码分析之不可控的堆外内存

概述

之前写过篇文章,关于堆外内存的,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关联的堆外内存其实是大于它的。

相关推荐