Java堆外內(nèi)存回收原理

Java 堆外內(nèi)存回收原理

簡(jiǎn)書(shū)滌生俺附。
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝溪掀!
如果讀完覺(jué)得有收獲的話(huà),歡迎點(diǎn)贊加關(guān)注揪胃。

DirectByteBuffer 簡(jiǎn)介

DirectByteBuffer 這個(gè)類(lèi)是 JDK 提供使用堆外內(nèi)存的一種途徑蛮浑,當(dāng)然常見(jiàn)的業(yè)務(wù)開(kāi)發(fā)一般不會(huì)接觸到只嚣,即使涉及到也可能是框架(如 Netty沮稚、RPC 等)使用的册舞,對(duì)框架使用者來(lái)說(shuō)也是透明的。

堆外內(nèi)存的優(yōu)勢(shì)

堆外內(nèi)存優(yōu)勢(shì)在 IO 操作上调鲸,對(duì)于網(wǎng)絡(luò) IO盛杰,使用 Socket 發(fā)送數(shù)據(jù)時(shí),能夠節(jié)省堆內(nèi)存到堆外內(nèi)存的數(shù)據(jù)拷貝即供,所以性能更高∮谖ⅲ看過(guò) Netty 源碼的同學(xué)應(yīng)該了解逗嫡,Netty 使用堆外內(nèi)存池來(lái)實(shí)現(xiàn)零拷貝技術(shù)株依。對(duì)于磁盤(pán) IO 時(shí),也可以使用內(nèi)存映射恋腕,來(lái)提升性能抹锄。
另外,更重要的幾乎不用考慮堆內(nèi)存煩人的 GC 問(wèn)題荠藤。

堆外內(nèi)存的創(chuàng)建

我們直接來(lái)看代碼,首先向 Bits 類(lèi)申請(qǐng)額度哈肖,Bits 類(lèi)內(nèi)部維護(hù)著當(dāng)前已經(jīng)使用的堆外內(nèi)存值吻育,會(huì) check 當(dāng)前申請(qǐng)的大小與已經(jīng)使用的內(nèi)存大小是否超過(guò)總的堆外內(nèi)存大心党埂(默認(rèn)大小與堆內(nèi)存差不多扫沼,其實(shí)是有細(xì)微區(qū)別的庄吼,拿 CMS GC 來(lái)舉例,它的大小是新生代的最大值 - 一個(gè) survivor 的大小 + 老生代的最大值)总寻,可以使用 -XX:MaxDirectMemorySize 參數(shù)指定堆外內(nèi)存最大大小器罐。

   //
    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 不通過(guò)渐行,會(huì)主動(dòng)執(zhí)行 System.gc()轰坊,然后 sleep 100 毫秒祟印,再進(jìn)行 check肴沫,如果內(nèi)存還是不足蕴忆,就拋出 OOM Error。

如果 check 通過(guò)套鹅,就會(huì)調(diào)用 unsafe.allocateMemory 真正分配內(nèi)存站蝠,返回內(nèi)存地址,然后再將內(nèi)存清 0卓鹿。題外話(huà),這個(gè) unsafe 命名看著是不是很?chē)樔艘魉铮@個(gè) unsafe 不是說(shuō)不安全澜倦,而是 JDK 內(nèi)部使用的類(lèi)杰妓,不推薦外部使用肥隆,所以叫 unsafe稚失,Netty 源碼內(nèi)部也有類(lèi)似命名。

由于申請(qǐng)內(nèi)存前可能會(huì)調(diào)用 System.gc()句各,所以謹(jǐn)慎設(shè)置 -XX:+DisableExplicitGC 這個(gè)選項(xiàng)吸占,這個(gè)參數(shù)作用是禁止代碼中顯示觸發(fā)的 Full GC。

堆外內(nèi)存的回收

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

看到這段代碼從成員的命名上就應(yīng)該知道矾屯,是用來(lái)回收堆外內(nèi)存的。確實(shí)初厚,但是它是如何工作的呢件蚕?接下來(lái)我們看看 Cleaner 類(lèi)。

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 類(lèi)排作,內(nèi)部維護(hù)了一個(gè) Cleaner 對(duì)象的鏈表牵啦,通過(guò) create(Object, Runnable) 方法創(chuàng)建 cleaner 對(duì)象妄痪,調(diào)用自身的 add 方法哈雏,將其加入到鏈表中衫生。
更重要的是提供了 clean 方法,clean 方法首先將對(duì)象自身從鏈表中刪除罪针,保證只調(diào)用一次彭羹,然后執(zhí)行 this.thunk 的 run 方法,thunk 就是由創(chuàng)建時(shí)傳入的 Runnable 參數(shù)泪酱,也就是說(shuō) clean 只負(fù)責(zé)觸發(fā) Runnable 的 run 方法,至于 Runnable 做什么任務(wù)它不關(guān)心西篓。

那 DirectByteBuffer 傳進(jìn)來(lái)的 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 類(lèi)的對(duì)象就是 DirectByteBuffer 中的 cleaner 傳進(jìn)來(lái)的 Runnable 參數(shù)類(lèi)岂津,我們直接看 run 方法 unsafe.freeMemory 釋放內(nèi)存,然后更新 Bits 里已使用的內(nèi)存數(shù)據(jù)吮成。

接下來(lái)我們關(guān)注各個(gè)環(huán)節(jié)是如何串起來(lái)的橱乱?這里主要講兩種回收方式:一種是自動(dòng)回收,一種是手動(dòng)回收粱甫。

如何自動(dòng)回收?

Java 是不用用戶(hù)去管理內(nèi)存的茶宵,所以 Java 對(duì)堆外內(nèi)存 默認(rèn)是自動(dòng)回收的危纫。
它是 由 GC 模塊負(fù)責(zé)的乌庶,在 GC 時(shí)會(huì)掃描 DirectByteBuffer 對(duì)象是否有有效引用指向該對(duì)象种蝶,如沒(méi)有瞒大,在回收 DirectByteBuffer 對(duì)象的同時(shí)且會(huì)回收其占用的堆外內(nèi)存螃征。但是 JVM 如何釋放其占用的堆外內(nèi)存呢透敌?如何跟 Cleaner 關(guān)聯(lián)起來(lái)呢踢械?

這得從 Cleaner 繼承了 PhantomReference(虛引用) 說(shuō)起。說(shuō)到 Reference魄藕,還有 SoftReference内列、WeakReference泼疑、FinalReference 他們作用各不相同,這里就不展開(kāi)說(shuō)了退渗。

簡(jiǎn)單介紹 PhantomReference,首先虛引用是不會(huì)影響 JVM 去回收其指向的對(duì)象蕴纳,當(dāng) GC 某個(gè)對(duì)象時(shí)会油,如果有此對(duì)象上還有虛引用對(duì)其引用古毛,會(huì)將 PhantomReference 對(duì)象插入 ReferenceQueue 隊(duì)列翻翩。

PhantomReference插入到哪個(gè)隊(duì)列呢稻薇?
看 PhantomReference 類(lèi)代碼嫂冻,其繼承自 Reference塞椎,Reference 對(duì)象有個(gè) ReferenceQueue 成員,這個(gè)也就是 PhantomReference 對(duì)象插入的 ReferenceQueue 隊(duì)列案狠,此成員如果不由外部傳入就是 ReferenceQueue.NULL服傍。如果需要通過(guò) queue 拿到 PhantomReference 對(duì)象,這個(gè) ReferenceQueue 對(duì)象還是必須由外部傳入骂铁。

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 類(lèi)內(nèi)部 static 靜態(tài)塊會(huì)啟動(dòng) ReferenceHandler 線(xiàn)程,線(xiàn)程優(yōu)先級(jí)很高拉庵,這個(gè)線(xiàn)程是用來(lái)處理 JVM 在 GC 過(guò)程中交接過(guò)來(lái)的 reference灿椅。想必經(jīng)常用 jstack 命令钞支,看線(xiàn)程堆棧的同學(xué)應(yīng)該見(jiàn)到過(guò)這個(gè)線(xiàn)程阱扬。


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

}

我們來(lái)看看 ReferenceHandler 是如何處理的伸辟?
直接看 run 方法,首先是個(gè)死循環(huán)信夫,一直在那不停的干活窃蹋,synchronized 塊內(nèi)的這段主要是交接 JVM 扔過(guò)來(lái)的 reference(就是 pending)卡啰,再往下看警没,很明顯,調(diào)用了 cleaner 的 clean 方法杀迹。調(diào)完之后直接 continue 結(jié)束此次循環(huán)亡脸,這個(gè) reference 并沒(méi)有進(jìn)入 queue,也就是說(shuō) 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);
            }
        }
    }

這塊有點(diǎn)想不通,既然不放入 ReferenceQueue续语,為什么 Cleaner 類(lèi)還是初始化了這個(gè) ReferenceQueue垂谢。

如何手動(dòng)回收疮茄?

手動(dòng)回收滥朱,就是由開(kāi)發(fā)手動(dòng)調(diào)用 DirectByteBuffer 的 cleaner 的 clean 方法來(lái)釋放空間力试。由于 cleaner 是 private 反問(wèn)權(quán)限徙邻,所以自然想到使用反射來(lái)實(shí)現(xiàn)畸裳。

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 實(shí)現(xiàn)了 DirectBuffer 接口躯畴,這個(gè)接口有 cleaner 方法可以獲取 cleaner 對(duì)象民鼓。

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

Netty 中的堆外內(nèi)存池就是使用反射來(lái)實(shí)現(xiàn)手動(dòng)回收方式進(jìn)行回收的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丰嘉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嚷缭,更是在濱河造成了極大的恐慌饮亏,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件路幸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡付翁,警方通過(guò)查閱死者的電腦和手機(jī)简肴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)百侧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)能扒,“玉大人,你說(shuō)我怎么就攤上這事辫狼〕醢撸” “怎么了膨处?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵见秤,是天一觀的道長(zhǎng)真椿。 經(jīng)常有香客問(wèn)我鹃答,道長(zhǎng)瀑粥,這世上最難降的妖魔是什么挣跋? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任狞换,我火速辦了婚禮,結(jié)果婚禮上舟肉,老公的妹妹穿的比我還像新娘修噪。我一直安慰自己路媚,他們只是感情好黄琼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布整慎。 她就那樣靜靜地躺著,像睡著了一般裤园。 火紅的嫁衣襯著肌膚如雪撤师。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天拧揽,我揣著相機(jī)與錄音,去河邊找鬼淤袜。 笑死痒谴,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的铡羡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼烦周,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼库倘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起教翩,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤杆勇,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后饱亿,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡彪笼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年钻注,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了配猫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幅恋。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泵肄,死狀恐怖捆交,靈堂內(nèi)的尸體忽然破棺而出腐巢,到底是詐尸還是另有隱情品追,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布肉瓦,位于F島的核電站,受9級(jí)特大地震影響胃惜,放射性物質(zhì)發(fā)生泄漏泞莉。R本人自食惡果不足惜船殉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一鲫趁、第九天 我趴在偏房一處隱蔽的房頂上張望捺弦。 院中可真熱鬧饮寞,春花似錦列吼、人聲如沸幽崩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蹄溉,卻和暖如春咨油,著一層夾襖步出監(jiān)牢的瞬間柒爵,已是汗流浹背役电。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工棉胀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留法瑟,地道東北人唁奢。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓霎挟,卻偏偏與公主長(zhǎng)得像麻掸,于是被迫代替她去往敵國(guó)和親酥夭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子熬北,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353