博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
JVM源码分析之不可控的堆外内存
阅读量:5947 次
发布时间:2019-06-19

本文共 5781 字,大约阅读时间需要 19 分钟。

概述

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

个人公众号:

39a917faff88034b56089cf3d899f1beff305b33

转载地址:http://cldxx.baihongyu.com/

你可能感兴趣的文章
并查集hdu1232
查看>>
oracle进行字符串拆分并组成数组
查看>>
100多个基础常用JS函数和语法集合大全
查看>>
Java8 lambda表达式10个示例
查看>>
innerHTML outerHTML innerText
查看>>
kafka安装教程
查看>>
go语言基础
查看>>
【Windows】字符串处理
查看>>
Spring(十八):Spring AOP(二):通知(前置、后置、返回、异常、环绕)
查看>>
CentOS使用chkconfig增加开机服务提示service xxx does not support chkconfig的问题解决
查看>>
微服务+:服务契约治理
查看>>
save
查看>>
Android DrawLayout + ListView 的使用(一)
查看>>
clear session on close of browser jsp
查看>>
关于吃掉物理的二次聚合无法实现的需要之旁门左道实现法
查看>>
mysql出现unblock with 'mysqladmin flush-hosts'
查看>>
oracle exp/imp命令详解
查看>>
开发安全的 API 所需要核对的清单
查看>>
Mycat源码中的单例模式
查看>>
WPF Dispatcher介绍
查看>>