關於DirectByteBuffer直接堆外內存釋放的理解

基本概念:

JVM可以使用的內存分外2種:堆內存和堆外內存,堆內存完全由JVM負責分配和釋放,如果程序沒有缺陷代碼導致內存泄露,那麼就不會遇到java.lang.OutOfMemoryError這個錯誤。使用堆外內存,就是爲了能直接分配和釋放內存,提高效率。

我的理解:

Unsafe.allocateMemory分配堆外內存,Unsafe.freeMemory釋放堆外內存,分配內存通過ByteBuffer.allocateDirect創建DirectByteBuffer對象並分配內存,同時記錄相關分配大小並創建回調函數Cleaner,記錄堆外內存分配大小的目的在於便於下一次分配的時候判斷內存是否夠用,不夠用了調用gc,但是gc只負責jvm內存回收,那是如何回收堆外內存的呢,答案是:創建回調函數的目的是gc調用之後,會調用DirectByteBuffer的回調對象Cleaner的clean()方法,進行內存回收,同時減去已使用堆外內存的大小

舉例分析

場景:設置虛擬機堆大小等於40M,並打印相關GC信息(idea Vm optinos設置: -verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=40M),每次分配10M大小,啓動,並跟蹤代碼:

public static void main(String[] args) {
    while (true) {
        ByteBuffer.allocateDirect(10 * 1024 * 1024);
    }
}

創建DirectByteBuffer對象

public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
DirectByteBuffer(int cap) {                  
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
// 記錄使用大小,判斷申請分配的堆外內存是否大於剩餘堆外內存,如果大於將會調用gc,之後分析
        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;
        }
// 虛引用PhantomReference, 在<<深入理解Java虛擬機>>一文中,
// 它唯一的目的就是爲一個對象設置虛引用關聯的唯一目的就是能在這
// 個對象被收集器回收時收到一個系統通知,Cleaner就是一個繼承了PhantomReference,
// 作爲一個回調的鉤子調用,最終當堆外內存不夠用當時候,而第二個參數Deallocator是一個繼承
// Thread的線程類,當調用Cleaner的clean()方法的時候,會執行Deallocator線程調用
// unsafe.freeMemory釋放堆外內存
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;

    }

        Bits.reserveMemory(size, cap);判斷堆外內存使用情況:

static void reserveMemory(long size, int cap) {

        // 省略
        // 判斷堆外內存使用情況
        if (tryReserveMemory(size, cap)) {
            return;
        }
        // 回調函數的引用(Cleaner類)
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
        // 堆外內存不夠用了執行gc,之後會調用回調函數clean(),進行堆外內存回收
        System.gc();

         // 省略
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                // 嘗試對堆外內存的分配進行判斷,直到滿足分配情況,停止循環
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                // 在這裏調用會調用到引用clean()方法
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }

            // no luck
            throw new OutOfMemoryError("Direct buffer memory");
        }
         // 省略

    }

判斷堆外內存分配情況如下圖,可以看到我們設置到堆外內存總大小maxMemory=41943040(40M),每次申請大小size=10M,已用大小totalCap=20M(循環了兩次),如果堆外內存不夠用了返回true,reserveMemory將執行System.gc()

接下來分析堆外內存不夠用之後,是否調用了回調函數clean(),答案是肯定的,會調用的

 public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

認真寫寫博客,寫寫生活點滴

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章