Java堆外內存回收原理

Java 堆外內存回收原理

簡書滌生
轉載請注明原創出處,謝謝!
如果讀完覺得有收獲的話,歡迎點贊加關注。

DirectByteBuffer 簡介

DirectByteBuffer 這個類是 JDK 提供使用堆外內存的一種途徑,當然常見的業務開發一般不會接觸到,即使涉及到也可能是框架(如 Netty、RPC 等)使用的,對框架使用者來說也是透明的。

堆外內存的優勢

堆外內存優勢在 IO 操作上,對于網絡 IO,使用 Socket 發送數據時,能夠節省堆內存到堆外內存的數據拷貝,所以性能更高。看過 Netty 源碼的同學應該了解,Netty 使用堆外內存池來實現零拷貝技術。對于磁盤 IO 時,也可以使用內存映射,來提升性能。
另外,更重要的幾乎不用考慮堆內存煩人的 GC 問題。

堆外內存的創建

我們直接來看代碼,首先向 Bits 類申請額度,Bits 類內部維護著當前已經使用的堆外內存值,會 check 當前申請的大小與已經使用的內存大小是否超過總的堆外內存大小(默認大小與堆內存差不多,其實是有細微區別的,拿 CMS GC 來舉例,它的大小是新生代的最大值 - 一個 survivor 的大小 + 老生代的最大值),可以使用 -XX:MaxDirectMemorySize 參數指定堆外內存最大大小。

   //
    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;

    }

如果 check 不通過,會主動執行 System.gc(),然后 sleep 100 毫秒,再進行 check,如果內存還是不足,就拋出 OOM Error。

如果 check 通過,就會調用 unsafe.allocateMemory 真正分配內存,返回內存地址,然后再將內存清 0。題外話,這個 unsafe 命名看著是不是很嚇人,這個 unsafe 不是說不安全,而是 JDK 內部使用的類,不推薦外部使用,所以叫 unsafe,Netty 源碼內部也有類似命名。

由于申請內存前可能會調用 System.gc(),所以謹慎設置 -XX:+DisableExplicitGC 這個選項,這個參數作用是禁止代碼中顯示觸發的 Full GC。

堆外內存的回收

cleaner = Cleaner.create(this, new Deallocator(base, size, cap));

看到這段代碼從成員的命名上就應該知道,是用來回收堆外內存的。確實,但是它是如何工作的呢?接下來我們看看 Cleaner 類。

public class Cleaner extends PhantomReference {
    private static final ReferenceQueue dummyQueue = new ReferenceQueue();
    private static Cleaner first = null;
    private Cleaner next = null;
    private Cleaner prev = null;
    private final Runnable thunk;

    private static synchronized Cleaner add(Cleaner var0) {
       ...
    }

    private static synchronized boolean remove(Cleaner var0) {
        ...
    }

    private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }

    public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null?null:add(new Cleaner(var0, var1));
    }

    public void clean() {
        if(remove(this)) {
            try {
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction() {
                    public Void run() {
                        if(System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
}

Cleaner 類,內部維護了一個 Cleaner 對象的鏈表,通過 create(Object, Runnable) 方法創建 cleaner 對象,調用自身的 add 方法,將其加入到鏈表中。
更重要的是提供了 clean 方法,clean 方法首先將對象自身從鏈表中刪除,保證只調用一次,然后執行 this.thunk 的 run 方法,thunk 就是由創建時傳入的 Runnable 參數,也就是說 clean 只負責觸發 Runnable 的 run 方法,至于 Runnable 做什么任務它不關心。

那 DirectByteBuffer 傳進來的 Runnable是什么呢?

 private static class Deallocator
        implements Runnable
    {

        private static Unsafe unsafe = Unsafe.getUnsafe();

        private long address;
        private long size;
        private int capacity;

        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);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

Deallocator 類的對象就是 DirectByteBuffer 中的 cleaner 傳進來的 Runnable 參數類,我們直接看 run 方法 unsafe.freeMemory 釋放內存,然后更新 Bits 里已使用的內存數據。

接下來我們關注各個環節是如何串起來的?這里主要講兩種回收方式:一種是自動回收,一種是手動回收。

如何自動回收?

Java 是不用用戶去管理內存的,所以 Java 對堆外內存 默認是自動回收的。
它是 由 GC 模塊負責的,在 GC 時會掃描 DirectByteBuffer 對象是否有有效引用指向該對象,如沒有,在回收 DirectByteBuffer 對象的同時且會回收其占用的堆外內存。但是 JVM 如何釋放其占用的堆外內存呢?如何跟 Cleaner 關聯起來呢?

這得從 Cleaner 繼承了 PhantomReference(虛引用) 說起。說到 Reference,還有 SoftReference、WeakReference、FinalReference 他們作用各不相同,這里就不展開說了。

簡單介紹 PhantomReference,首先虛引用是不會影響 JVM 去回收其指向的對象,當 GC 某個對象時,如果有此對象上還有虛引用對其引用,會將 PhantomReference 對象插入 ReferenceQueue 隊列。

PhantomReference插入到哪個隊列呢?
看 PhantomReference 類代碼,其繼承自 Reference,Reference 對象有個 ReferenceQueue 成員,這個也就是 PhantomReference 對象插入的 ReferenceQueue 隊列,此成員如果不由外部傳入就是 ReferenceQueue.NULL。如果需要通過 queue 拿到 PhantomReference 對象,這個 ReferenceQueue 對象還是必須由外部傳入。

private static final ReferenceQueue dummyQueue = new ReferenceQueue();

private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
}

public class PhantomReference<T> extends Reference<T> {

Reference 類內部 static 靜態塊會啟動 ReferenceHandler 線程,線程優先級很高,這個線程是用來處理 JVM 在 GC 過程中交接過來的 reference。想必經常用 jstack 命令,看線程堆棧的同學應該見到過這個線程。


public abstract class Reference<T> {

   
    private T referent;         /* Treated specially by GC */

    ReferenceQueue<? super T> queue;

    Reference next;
    transient private Reference<T> discovered;  /* used by VM */

    static private class Lock { };
    private static Lock lock = new Lock();
    private static Reference pending = null;
    
    ...

    static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();
    }

    public T get() {
        return this.referent;
    }

    public void clear() {
        this.referent = null;
    }

    public boolean isEnqueued() {
            synchronized (this) {
            return (this.queue != ReferenceQueue.NULL) && (this.next != null);
        }
    }

  
    public boolean enqueue() {
        return this.queue.enqueue(this);
    }


    /* -- Constructors -- */

    Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }

}

我們來看看 ReferenceHandler 是如何處理的?
直接看 run 方法,首先是個死循環,一直在那不停的干活,synchronized 塊內的這段主要是交接 JVM 扔過來的 reference(就是 pending),再往下看,很明顯,調用了 cleaner 的 clean 方法。調完之后直接 continue 結束此次循環,這個 reference 并沒有進入 queue,也就是說 Cleaner 虛引用是不放入 ReferenceQueue。

/* High-priority thread to enqueue pending References
     */
    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();
                    continue;
                }

                ReferenceQueue q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
            }
        }
    }

這塊有點想不通,既然不放入 ReferenceQueue,為什么 Cleaner 類還是初始化了這個 ReferenceQueue。

如何手動回收?

手動回收,就是由開發手動調用 DirectByteBuffer 的 cleaner 的 clean 方法來釋放空間。由于 cleaner 是 private 反問權限,所以自然想到使用反射來實現。

public static void clean(final ByteBuffer byteBuffer) {  
 if (byteBuffer.isDirect()) { 
        Field cleanerField = byteBuffer.getClass().getDeclaredField("cleaner");
        cleanerField.setAccessible(true);
        Cleaner cleaner = (Cleaner) cleanerField.get(byteBuffer);
        cleaner.clean();
    }
}

還有另一種方法,DirectByteBuffer 實現了 DirectBuffer 接口,這個接口有 cleaner 方法可以獲取 cleaner 對象。

public static void clean(final ByteBuffer byteBuffer) {  
    if (byteBuffer.isDirect()) {  
        ((DirectBuffer)byteBuffer).cleaner().clean();  
    }  
}

Netty 中的堆外內存池就是使用反射來實現手動回收方式進行回收的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379