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生態報告

相關文章
相關標籤/搜索