深入理解DirectBuffer

介紹

? ? 最近在工作中使用到了DirectBuffer來(lái)進(jìn)行臨時(shí)數(shù)據(jù)的存放硅确,由于使用的是堆外內(nèi)存,省去了數(shù)據(jù)到內(nèi)核的拷貝明肮,因此效率比用ByteBuffer要高不少菱农。之前看過(guò)許多介紹DirectBuffer的文章,在這里從源碼的角度上來(lái)看一下DirectBuffer的原理晤愧。

用戶態(tài)和內(nèi)核態(tài)

? ? Intel的 X86架構(gòu)下大莫,為了實(shí)現(xiàn)外部應(yīng)用程序與操作系統(tǒng)運(yùn)行時(shí)的隔離,分為了Ring0-Ring3四種級(jí)別的運(yùn)行模式官份。Linux/Unix只使用了Ring0和Ring3兩個(gè)級(jí)別只厘。Ring0被稱為用戶態(tài),Ring3被稱為內(nèi)核態(tài)舅巷。普通的應(yīng)用程序只能運(yùn)行在Ring3羔味,并且不能訪問(wèn)Ring0的地址空間。操作系統(tǒng)運(yùn)行在Ring0钠右,并提供系統(tǒng)調(diào)用供用戶態(tài)的程序使用赋元。如果用戶態(tài)的程序的某一個(gè)操作需要內(nèi)核態(tài)來(lái)協(xié)助完成(例如讀取磁盤(pán)上的某一段數(shù)據(jù)),那么用戶態(tài)的程序就會(huì)通過(guò)系統(tǒng)調(diào)用來(lái)調(diào)用內(nèi)核態(tài)的接口,請(qǐng)求操作系統(tǒng)來(lái)完成某種操作搁凸。

? ? 下圖是用戶態(tài)調(diào)用內(nèi)核態(tài)的示意圖:

系統(tǒng)調(diào)用.jpg

DirectBuffer的創(chuàng)建

? ? 使用下面一行代碼就可以創(chuàng)建一個(gè)1024字節(jié)的DirectBuffer:


ByteBuffer.allocateDirect(1024);

? ? 該方法調(diào)用的是new DirectByteBuffer(int cap)媚值。DirectByteBuffer的構(gòu)造函數(shù)是包級(jí)私有的,因此外部是調(diào)用不到的护糖。

下面我們來(lái)看一下這行代碼背后的邏輯:


DirectByteBuffer(int cap) {                  // package-private

    super(-1, 0, cap, cap);

    boolean pa = VM.isDirectMemoryPageAligned();  //是否頁(yè)對(duì)齊

    int ps = Bits.pageSize();    //獲取pageSize大小

    long size = Math.max(1L, (long) cap + (pa ? ps : 0));  //如果是頁(yè)對(duì)齊的話褥芒,那么就加上一頁(yè)的大小

    Bits.reserveMemory(size, cap);  //對(duì)分配的直接內(nèi)存做一個(gè)記錄

    long base = 0;

    try {

        base = unsafe.allocateMemory(size);  //實(shí)際分配內(nèi)存

    } catch (OutOfMemoryError x) {

        Bits.unreserveMemory(size, cap);

        throw x;

    }

    unsafe.setMemory(base, size, (byte) 0);  //初始化內(nèi)存

    //計(jì)算地址

    if (pa && (base % ps != 0)) {

        // Round up to page boundary

        address = base + ps - (base & (ps - 1));

    } else {

        address = base;

    }

    //生成Cleaner

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

    att = null;

}

? ? DirectBuffer的構(gòu)造函數(shù)主要做以下三個(gè)事情:
1、根據(jù)頁(yè)對(duì)齊和pageSize來(lái)確定本次的要分配內(nèi)存實(shí)際大小
2嫡良、實(shí)際分配內(nèi)存锰扶,并且記錄分配的內(nèi)存大小
3、聲明一個(gè)Cleaner對(duì)象用于清理該DirectBuffer內(nèi)存

需要注意的是DirectBuffer的創(chuàng)建是比較耗時(shí)的寝受,所以在一些高性能的中間件或者應(yīng)用下一般會(huì)做一個(gè)對(duì)象池坷牛,用于重復(fù)利用DirectBuffer。

DirectBuffer的使用

? ? 查看DirectBuffer類(lèi)的方法聲明很澄,對(duì)于DirectBuffer的使用主要有兩類(lèi)方法京闰,putXXX和getXXX。

putXXX方法(以putInt為例):


public ByteBuffer putInt(int x) {

    putInt(ix(nextPutIndex((1 << 2))), x);

    return this;

}

private ByteBuffer putInt(long a, int x) {

    if (unaligned) {

        int y = (x);

        unsafe.putInt(a, (nativeByteOrder ? y : Bits.swap(y)));

    } else {

        Bits.putInt(a, x, bigEndian);

    }

    return this;

}

? ? putInt方法會(huì)根據(jù)是否是內(nèi)存對(duì)齊分別調(diào)用unsafe.putInt或者Bits.putInt來(lái)把數(shù)據(jù)放到直接內(nèi)存中痴怨。Bits.putInt實(shí)際上會(huì)根據(jù)是大端或者是小端來(lái)區(qū)分如何把數(shù)據(jù)放到直接內(nèi)存中忙干,放的方式同樣是調(diào)用unsage.putInt。

getXXX方法(以getInt為例):


public int getInt() {

    return getInt(ix(nextGetIndex((1 << 2))));

}

private int getInt(long a) {

    if (unaligned) {

        int x = unsafe.getInt(a);

        return (nativeByteOrder ? x : Bits.swap(x));

    }

    return Bits.getInt(a, bigEndian);

}

? ? 首先判斷是否是頁(yè)對(duì)齊浪藻,如果不是頁(yè)對(duì)齊捐迫,那么直接通過(guò)unsafe.getInt來(lái)獲取數(shù)據(jù);如果是頁(yè)對(duì)齊爱葵,那么通過(guò)Bits.getInt方法來(lái)獲取數(shù)據(jù)施戴。Bits.getInt同樣是根據(jù)大端還是小端,調(diào)用unsafe.getInt來(lái)獲取數(shù)據(jù)萌丈。

DirectBuffer內(nèi)存回收

? ? DirectBuffer內(nèi)存回收主要有兩種方式赞哗,一種是通過(guò)System.gc來(lái)回收,另一種是通過(guò)構(gòu)造函數(shù)里創(chuàng)建的Cleaner對(duì)象來(lái)回收辆雾。

System.gc回收

在DirectBuffer的構(gòu)造函數(shù)中肪笋,用到了Bit.reserveMemory這個(gè)方法,該方法如下

static void reserveMemory(long size, int cap) {

        ······

        if (tryReserveMemory(size, cap)) {

            return;

        }

        ······

        while (jlra.tryHandlePendingReference()) {

            if (tryReserveMemory(size, cap)) {

                return;

            }

        }



        System.gc();

        // a retry loop with exponential back-off delays

        // (this gives VM some time to do it's job)

        boolean interrupted = false;

        try {

            long sleepTime = 1;

            int sleeps = 0;

            while (true) {

                if (tryReserveMemory(size, cap)) {

                    return;

                }

                if (sleeps >= MAX_SLEEPS) {

                    break;

                }

                if (!jlra.tryHandlePendingReference()) {

                    try {

                        Thread.sleep(sleepTime);

                        sleepTime <<= 1;

                        sleeps++;

                    } catch (InterruptedException e) {

                        interrupted = true;

                    }

                }

            }

            // no luck

            throw new OutOfMemoryError("Direct buffer memory");

        } finally {

            if (interrupted) {

                // don't swallow interrupts

                Thread.currentThread().interrupt();

            }

        }

    }

? ? reserveMemory方法首先嘗試分配內(nèi)存度迂,如果分配成功的話藤乙,那么就直接退出。如果分配失敗那么就通過(guò)調(diào)用tryHandlePendingReference來(lái)嘗試清理堆外內(nèi)存(最終調(diào)用的是Cleaner的clean方法惭墓,其實(shí)就是unsafe.freeMemory然后釋放內(nèi)存)坛梁,清理完內(nèi)存之后再嘗試分配內(nèi)存。如果還是失敗腊凶,調(diào)用System.gc()來(lái)觸發(fā)一次FullGC進(jìn)行回收(前提是沒(méi)有加-XX:-+DisableExplicitGC參數(shù))划咐。GC完之后再進(jìn)行內(nèi)存分配拴念,失敗的話就會(huì)進(jìn)行sleep,然后再進(jìn)行嘗試褐缠。每次sleep的時(shí)間是逐步增加的政鼠,規(guī)律是1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)。如果最終還沒(méi)有可分配的內(nèi)存送丰,那么就會(huì)拋出OOM異常缔俄。

? ? 為什么是通過(guò)調(diào)用tryHandlePendingReference來(lái)回收內(nèi)存呢?答案是JVM在判斷內(nèi)存不可達(dá)之后會(huì)把需要GC的不可達(dá)對(duì)象放在一個(gè)PendingList中器躏,然后應(yīng)用程序就可以看到這些對(duì)象。通過(guò)調(diào)用tryHandlePendingReference來(lái)訪問(wèn)這些不可達(dá)對(duì)象蟹略。如果不可達(dá)對(duì)象是Cleaner類(lèi)型登失,也就是說(shuō)關(guān)聯(lián)了堆外的DirectBuffer,那么該DirectBuffer就可以被回收了挖炬,通過(guò)調(diào)用Cleaner的clean方法來(lái)回收這部分堆外內(nèi)存揽浙。

這個(gè)邏輯就是進(jìn)行堆外內(nèi)存分配時(shí)觸發(fā)的回收內(nèi)存邏輯,也就是說(shuō)在分配的時(shí)候如果遇到堆外內(nèi)存不足意敛,可能會(huì)觸發(fā)FullGC馅巷,然后嘗試進(jìn)行分配。這也是為什么在一些用到堆外內(nèi)存的應(yīng)用中不建議加上-XX:-+DisableExplicitGC參數(shù)草姻。

Cleaner對(duì)象回收

? ? 另個(gè)觸發(fā)堆外內(nèi)存回收的時(shí)機(jī)是通過(guò)Cleaner對(duì)象的clean方法進(jìn)行回收钓猬。在每次新建一個(gè)DirectBuffer對(duì)象的時(shí)候,會(huì)同時(shí)創(chuàng)建一個(gè)Cleaner對(duì)象撩独,同一個(gè)進(jìn)程創(chuàng)建的所有的DirectBuffer對(duì)象跟Cleaner對(duì)象的個(gè)數(shù)是一樣的敞曹,并且所有的Cleaner對(duì)象會(huì)組成一個(gè)鏈表,前后相連综膀。


public static Cleaner create(Object ob, Runnable thunk) {

        if (thunk == null)

            return null;

        return add(new Cleaner(ob, thunk));

    }

? ? Cleaner對(duì)象的clean方法執(zhí)行時(shí)機(jī)是JVM在判斷該Cleaner對(duì)象關(guān)聯(lián)的DirectBuffer已經(jīng)不被任何對(duì)象引用了(也就是經(jīng)過(guò)可達(dá)性分析判定為不可達(dá)的時(shí)候)澳迫。此時(shí)Cleaner對(duì)象會(huì)被JVM掛到PendingList上。然后有一個(gè)固定的線程掃描這個(gè)List剧劝,如果遇到Cleaner對(duì)象橄登,那么就執(zhí)行clean方法。

? ? ? DirectBuffer在一些高性能的中間件上使用還是相當(dāng)廣泛的讥此。正確的使用可以提升程序的性能拢锹,降低GC的頻率。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末暂论,一起剝皮案震驚了整個(gè)濱河市面褐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌取胎,老刑警劉巖展哭,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件湃窍,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡匪傍,警方通過(guò)查閱死者的電腦和手機(jī)您市,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)役衡,“玉大人茵休,你說(shuō)我怎么就攤上這事∈中” “怎么了榕莺?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)棵介。 經(jīng)常有香客問(wèn)我钉鸯,道長(zhǎng),這世上最難降的妖魔是什么邮辽? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任唠雕,我火速辦了婚禮,結(jié)果婚禮上吨述,老公的妹妹穿的比我還像新娘岩睁。我一直安慰自己,他們只是感情好揣云,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布捕儒。 她就那樣靜靜地躺著,像睡著了一般灵再。 火紅的嫁衣襯著肌膚如雪肋层。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天翎迁,我揣著相機(jī)與錄音栋猖,去河邊找鬼。 笑死汪榔,一個(gè)胖子當(dāng)著我的面吹牛蒲拉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播痴腌,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼雌团,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了士聪?” 一聲冷哼從身側(cè)響起锦援,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎剥悟,沒(méi)想到半個(gè)月后灵寺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體曼库,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年略板,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了毁枯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡叮称,死狀恐怖种玛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瓤檐,我是刑警寧澤赂韵,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站距帅,受9級(jí)特大地震影響右锨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碌秸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悄窃。 院中可真熱鬧讥电,春花似錦、人聲如沸轧抗。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)横媚。三九已至纠炮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間灯蝴,已是汗流浹背恢口。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留穷躁,地道東北人耕肩。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像问潭,于是被迫代替她去往敵國(guó)和親猿诸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1.一些概念 1.1.數(shù)據(jù)類(lèi)型 Java虛擬機(jī)中狡忙,數(shù)據(jù)類(lèi)型可以分為兩類(lèi):基本類(lèi)型和引用類(lèi)型梳虽。基本類(lèi)型的變量保存原始...
    落落落落大大方方閱讀 4,524評(píng)論 4 86
  • 第十一章 持有對(duì)象 Java實(shí)用類(lèi)庫(kù)還提供了一套相當(dāng)完整的容器類(lèi)來(lái)解決這個(gè)問(wèn)題灾茁,其中基本的類(lèi)型是List窜觉、Set谷炸、...
    Lisy_閱讀 795評(píng)論 0 1
  • Java 虛擬機(jī)有自己完善的硬件架構(gòu), 如處理器、堆棧竖螃、寄存器等淑廊,還具有相應(yīng)的指令系統(tǒng)。JVM 屏蔽了與具體操作系...
    尹小凱閱讀 1,685評(píng)論 0 10
  • # 單純的筆記特咆,格式盡量工整 88.find find [路徑] -name"搜索條件" # 查找指定路徑下...
    學(xué)習(xí)UI閱讀 219評(píng)論 0 0
  • 有一親戚腻格,說(shuō)來(lái)廣州找我画拾,當(dāng)時(shí)我一聽(tīng),心里怕怕的菜职,反問(wèn)他過(guò)來(lái)找我干嘛青抛?他說(shuō)他想過(guò)來(lái)廣州工作,想讓我?guī)退夜ぷ鞒旰耍艺f(shuō)我...
    周公子聊娛樂(lè)閱讀 413評(píng)論 0 1