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

概述

以前写过篇文章,关于堆外内存的,JVM源码分析之堆外内存彻底解读,里面重点讲了DirectByteBuffer的原理,可是今天碰到一个比较奇怪的问题,在设置了-XX:MaxDirectMemorySize=1G的前提下,而后统计全部DirectByteBuffer对象后面占用的内存达到了7G,远远超出阈值,这个问题很诡异,因而好好查了下缘由,虽然最终发现是咱们统计的问题,可是期间发现的其余一些问题仍是值得分享一下的。java

不得不提的DirectByteBuffer构造函数

打开DirectByteBuffer这个类,咱们会发现有5个构造函数bash

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方法app

public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
}
复制代码

也就是会使用上面提到的第一个构造函数,即jvm

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)方法会作堆外内存的阈值checkide

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,那这种状况下他们怎么被回收呢?ui

那下面请你们先看下DirectByteBuffer里的这两个函数:this

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的全新副本,各类指针都同样。spa

所以从这个实现来看,后面关联的堆外内存实际上是同一块,因此若是咱们作统计的时候若是仅仅将全部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关联的堆外内存实际上是大于它的。

欢迎关注 PerfMa 社区,推荐阅读:

刨根问底——记一次 OOM 试验形成的电脑雪崩引起的思考

重磅:解读2020年最新JVM生态报告

相关文章
相关标签/搜索