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

网友投稿 932 2022-10-20

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

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关联的堆外内存其实是大于它的。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:一个精简的 Android ORM 框架
下一篇:基于Feign传输对象无法接收参数的问题
相关文章

 发表评论

暂时没有评论,来抢沙发吧~