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