堆外內(nèi)存整理

堆外內(nèi)存, JDK 1.4 nio引進(jìn)了ByteBuffer.allocateDirect()分配堆外內(nèi)存

  • ByteBuffer
    public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
    }

  • DirectByteBuffer
    DirectByteBuffer(int cap) {// package-private
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();//內(nèi)存是否按頁分配對齊
    int ps = Bits.pageSize();//獲取每頁內(nèi)存大小
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));//分配內(nèi)存的大小,如果是按頁對齊方式,需要再加一頁內(nèi)存的容量
    重點(diǎn):分配內(nèi)存和釋放內(nèi)存之前必須調(diào)用此方法
    Bits.reserveMemory(size, cap);//用Bits類保存總分配內(nèi)存(按頁分配)的大小和實(shí)際內(nèi)存的大小
    long base = 0;
    try {//在堆外內(nèi)存的基地址,指定內(nèi)存大小
    base = unsafe.allocateMemory(size);//unsafe.cpp中調(diào)用os::malloc分配內(nèi)存
    } catch (OutOfMemoryError x) {
    Bits.unreserveMemory(size, cap);
    throw x;
    }
    unsafe.setMemory(base, size, (byte) 0);
    if (pa && (base % ps != 0)) {//計(jì)算堆外內(nèi)存的基地址
    // 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;
    }

  • Deallocator
    private static class Deallocator implements Runnable
    {
    private static Unsafe unsafe = Unsafe.getUnsafe();
    private long address;//基地址
    private long size;//保存了堆外內(nèi)存的數(shù)據(jù)(開始地址、大小和容量)
    private int capacity;//保存了堆外內(nèi)存的數(shù)據(jù)(開始地址、大小和容量)
    private Deallocator(long address, long size, int capacity) {
    assert (address != 0);
    this.address = address;
    this.size = size;
    this.capacity = capacity;
    }
    public void run() {
    if (address == 0) {
    // Paranoia
    return;
    }
    unsafe.freeMemory(address);//調(diào)用OS的方法釋放地址,os::free
    address = 0;
    Bits.unreserveMemory(size, capacity);//統(tǒng)計(jì)堆外內(nèi)存大小
    }
    }

  • Cleaner
    public class Cleaner extends PhantomReference<Object> {
    private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();//static數(shù)據(jù)
    private static Cleaner first = null;//static數(shù)據(jù)
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;//Deallocator對象,每個cleaner對象都保留了一個Deallocator對象,它里面有address基地址等
    private static synchronized Cleaner add(Cleaner var0) {
    if(first != null) {
    var0.next = first;
    first.prev = var0;
    }
    first = var0;
    return var0;
    }
    private Cleaner(Object var1, Runnable var2) {
    super(var1, dummyQueue);//var1 傳的是DirectByteBuffer對象
    this.thunk = var2;//Deallocator對象
    }
    public static Cleaner create(Object var0, Runnable var1) {
    return var1 == null?null:add(new Cleaner(var0, var1));//var0傳的是DirectByteBuffer對象
    }

  • Bits
    // -- Direct memory management --
    // A user-settable upper limit on the maximum amount of allocatable direct buffer memory.
    // This value may be changed during VM initialization if it is launched with "-XX:MaxDirectMemorySize=<size>".
    private static volatile long maxMemory = VM.maxDirectMemory();
    private static volatile long reservedMemory;
    private static volatile long totalCapacity;
    private static volatile long count;
    private static boolean memoryLimitSet = false;
    // These methods should be called whenever direct memory is allocated or
    // freed. They allow the user to control the amount of direct memory
    // which a process may access. All sizes are specified in bytes.
    static void reserveMemory(long size, int cap) {
    synchronized (Bits.class) {
    if (!memoryLimitSet && VM.isBooted()) {
    maxMemory = VM.maxDirectMemory();// 67108864L == 64MB
    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();//內(nèi)存不夠了, try 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++;
    }
    }
    static synchronized void unreserveMemory(long size, int cap) {
    if (reservedMemory > 0) {
    reservedMemory -= size;
    totalCapacity -= cap;
    count--;
    assert (reservedMemory > -1);
    }
    }

  • DirectByteBuffer被回收

    DirectByteBuffer對象在創(chuàng)建的時候關(guān)聯(lián)了一個PhantomReference,說到PhantomReference它其實(shí)主要是用來跟蹤對象何時被回收的,
    它不能影響gc決策,但是gc過程中如果發(fā)現(xiàn)某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了,
    那將會把這個引用(Cleaner)放到j(luò)ava.lang.ref.Reference.pending隊(duì)列里,
    在gc完畢的時候通知ReferenceHandler這個守護(hù)線程去執(zhí)行一些后置處理,
    而DirectByteBuffer關(guān)聯(lián)的PhantomReference是PhantomReference的一個子類,
    在最終的處理里會通過Unsafe的free接口來釋放DirectByteBuffer對應(yīng)的堆外內(nèi)存塊

  • JDK里面的ReferenceHandler實(shí)現(xiàn)
    private static class ReferenceHandler extends Thread {
    ReferenceHandler(ThreadGroup g, String name) {
    super(g, name);
    }
    public void run() {
    for (;;) {
    Reference r;
    synchronized (lock) {
    if (pending != null) {
    r = pending;
    Reference rn = r.next;
    pending = (rn == r) ? null : rn;
    r.next = r;
    } else {
    try {
    lock.wait();
    } catch (InterruptedException x) { }
    continue;
    }
    }
    // Fast path for cleaners
    if (r instanceof Cleaner) {
    ((Cleaner)r).clean();//直接調(diào)用clean方法清理
    continue;
    }
    ReferenceQueue q = r.queue;
    if (q != ReferenceQueue.NULL) q.enqueue(r);
    }
    }
    }

  • 簡單流程梳理

    • 堆外內(nèi)存的申請
      • ByteBuffer.allocateDirect()
      • unsafe.allocateMemory()
      • os::malloc()
    • 堆外內(nèi)存的釋放
      • cleaner.clean()
        • 把自身從Clener鏈表刪除,從而在下次GC時能夠被回收
        • 釋放堆外內(nèi)存
      • unsafe.freeMemory()
      • os::free()
  • 對象的引用關(guān)系

    • 初始化時
    • 如果該DirectByteBuffer對象在一次GC中被回收了
  • 不過很多線上環(huán)境的JVM參數(shù)有-XX:+DisableExplicitGC,導(dǎo)致了System.gc()等于一個空函數(shù),根本不會觸發(fā)FGC,這一點(diǎn)在使用Netty框架時需要注意是否會出問題

  • 關(guān)于直接內(nèi)存默認(rèn)值是否為64MB?

    • java.lang.System
      private static void initializeSystemClass() {//Initialize the system class. Called after thread initialization.
      ...
      sun.misc.VM.saveAndRemoveProperties(props);
      ...
      }
    • saveAndRemoveProperties(){
      // Set the maximum amount of direct memory. This value is controlled
      // by the vm option -XX:MaxDirectMemorySize=<size>.
      // The maximum amount of allocatable direct buffer memory (in bytes)
      // from the system property sun.nio.MaxDirectMemorySize set by the VM.
      // The system property will be removed.
      String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
      if (s != null) {
      if (s.equals("-1")) {
      // -XX:MaxDirectMemorySize not given, take default
      directMemory = Runtime.getRuntime().maxMemory();
      } else {
      long l = Long.parseLong(s);
      if (l > -1)
      directMemory = l;
      }
      }}
    • 如果我們通過-Dsun.nio.MaxDirectMemorySize指定了這個屬性,只要它不等于-1,那效果和加了-XX:MaxDirectMemorySize一樣的,如果兩個參數(shù)都沒指定,那么最大堆外內(nèi)存的值來自于directMemory = Runtime.getRuntime().maxMemory(),這是一個native方法
    • Universe::heap()->max_capacity();
    • 其中在我們使用CMS GC的情況下的實(shí)現(xiàn)如下,其實(shí)是新生代的最大值-一個survivor的大小+老生代的最大值,也就是我們設(shè)置的-Xmx的值里除去一個survivor的大小就是默認(rèn)的堆外內(nèi)存的大小了
  • 如果發(fā)現(xiàn)某個對象除了只有PhantomReference引用它之外,并沒有其他的地方引用它了,那將會把這個引用放到j(luò)ava.lang.ref.Reference.pending隊(duì)列里,在gc完畢的時候通知ReferenceHandler這個守護(hù)線程去執(zhí)行一些后置處理

  • 可見如果pending為空的時候,會通過lock.wait()一直等在那里,其中喚醒的動作是在jvm里做的,當(dāng)gc完成之后會調(diào)用如下的方法VM_GC_Operation::doit_epilogue(),在方法末尾會調(diào)用lock的notify操作,至于pending隊(duì)列什么時候?qū)⒁梅胚M(jìn)去的,其實(shí)是在gc的引用處理邏輯中放進(jìn)去的,針對引用的處理后面可以專門寫篇文章來介紹

  • 對于System.gc的實(shí)現(xiàn),它會對新生代和老生代都會進(jìn)行內(nèi)存回收,這樣會比較徹底地回收DirectByteBuffer對象以及他們關(guān)聯(lián)的堆外內(nèi)存,我們dump內(nèi)存發(fā)現(xiàn)DirectByteBuffer對象本身其實(shí)是很小的,但是它后面可能關(guān)聯(lián)了一個非常大的堆外內(nèi)存,因此我們通常稱之為『冰山對象』,我們做ygc的時候會將新生代里的不可達(dá)的DirectByteBuffer對象及其堆外內(nèi)存回收了,但是無法對old里的DirectByteBuffer對象及其堆外內(nèi)存進(jìn)行回收,這也是我們通常碰到的最大的問題,如果有大量的DirectByteBuffer對象移到了old,但是又一直沒有做cms gc或者full gc,而只進(jìn)行ygc,那么我們的物理內(nèi)存可能被慢慢耗光,但是我們還不知道發(fā)生了什么,因?yàn)閔eap明明剩余的內(nèi)存還很多(前提是我們禁用了System.gc)。

  • 我們通信過程中如果數(shù)據(jù)是在Heap里的,最終也還是會copy一份到堆外,然后再進(jìn)行發(fā)送,所以為什么不直接使用堆外內(nèi)存呢

  • gc機(jī)制與堆外內(nèi)存的關(guān)系也說了,如果一直觸發(fā)不了cms gc或者full gc,那么后果可能很嚴(yán)重

  • References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容