美文网首页jvm
JVM源码分析之不可控的堆外内存

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

作者: taj3991 | 来源:发表于2020-03-03 17:55 被阅读0次

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

    原文

    http://lovestblog.cn/blog/2016/06/29/ooc-offheap/

    相关文章

      网友评论

        本文标题:JVM源码分析之不可控的堆外内存

        本文链接:https://www.haomeiwen.com/subject/gufklhtx.html