JAVA堆外內(nèi)存的簡介和使用

內(nèi)存分析

最近看了一篇文章《螞蟻消息中間件 (MsgBroker) 在 YGC 優(yōu)化上的探索》

文章涉及JVM的垃圾回收,主要講的是通過使用「堆外內(nèi)存」對Young GC進行優(yōu)化虹统。

文章中介紹染簇,MsgBroker消息中間件會對消息進行緩存,JVM需要為被緩存的消息分配內(nèi)存模燥,首先會被分配到年輕代咖祭。

當緩存中的消息由于各種原因,一直投遞不成功蔫骂,這些消息會進入老年代么翰。

最終呈現(xiàn)的問題是YGC時間太長。

隨著新特性的開發(fā)和消息量的增長辽旋,我們發(fā)現(xiàn) MsgBroker 的 YGC 平均耗時已緩慢增長至 50ms~60ms浩嫌,甚至部分機房的 YGC 平均耗時已高達 120ms。

有一個疑問补胚,消息進入老年代码耐,出現(xiàn)堆積,為何會導致YGC時間過長呢溶其?

按著文章中的敘述骚腥,回答這個問題。

  1. 在YGC階段瓶逃,涉及到垃圾標記的過程束铭,從GCRoot開始標記廓块。
  2. 因為YGC不涉及到老年代的回收,一旦從GCRoot掃描到引用了老年代對象時契沫,就中斷本次掃描带猴。這樣做可以減少掃描范圍,加速YGC埠褪。
  3. 存在被老年代對象引用的年輕代對象浓利,它們沒有被GCRoot直接或者間接引用。
  4. YGC階段中的old-gen scanning即用于掃描被老年代引用的年輕代對象钞速。
  5. old-gen scanning掃描時間與老年代內(nèi)存占用大小成正比贷掖。
  6. 得到結(jié)論,老年代內(nèi)存占用增大會導致YGC時間變長渴语。

總的來說苹威,將消息緩存在JVM內(nèi)存會對垃圾回收造成一定影響:

  1. 消息最初緩存到年輕代,會增加YGC的頻率驾凶。
  2. 消息被提升到老年代牙甫,會增加FGC的頻率。
  3. 老年代的消息增長后调违,會延長old-gen scanning時間窟哺,從而增加YGC耗時。

文章使用「堆外內(nèi)存」減少了消息對JVM內(nèi)存的占用技肩,并使用基于Netty的網(wǎng)絡層框架且轨,達到了理想的YGC時間。

注:Netty中也使用了堆外內(nèi)存虚婿。

通過引入自適應投遞限流旋奢,在實驗室測試環(huán)境下,MsgBroker 在異常場景下的 YGC 耗時進一步從 83ms 降低到 40ms然痊,恢復了正常的水平至朗。


一:堆外內(nèi)存是什么?

在JAVA中剧浸,JVM內(nèi)存指的是堆內(nèi)存锹引。

機器內(nèi)存中,不屬于堆內(nèi)存的部分即為堆外內(nèi)存辛蚊。

堆外內(nèi)存也被稱為直接內(nèi)存粤蝎。

堆內(nèi)存和堆外內(nèi)存

堆外內(nèi)存并不神秘,在C語言中袋马,分配的就是機器內(nèi)存,和本文中的堆外內(nèi)存是相似的概念秸应。

在JAVA中虑凛,可以通過Unsafe和NIO包下的ByteBuffer來操作堆外內(nèi)存碑宴。

Unsafe類操作堆外內(nèi)存

sun.misc.Unsafe提供了一組方法來進行堆外內(nèi)存的分配,重新分配桑谍,以及釋放延柠。

  1. public native long allocateMemory(long size); —— 分配一塊內(nèi)存空間。
  2. public native long reallocateMemory(long address, long size); —— 重新分配一塊內(nèi)存锣披,把數(shù)據(jù)從address指向的緩存中拷貝到新的內(nèi)存塊贞间。
  3. public native void freeMemory(long address); —— 釋放內(nèi)存。

參考:Unsafe類操作JAVA內(nèi)存

一頓操作猛如虎雹仿,直接psvm走起增热。

public static void main(String[] args) {
    Unsafe unsafe = new Unsafe();
    unsafe.allocateMemory(1024);
}

然而Unsafe類的構(gòu)造器是私有的,報錯胧辽。

而且峻仇,allocateMemory方法也不是靜態(tài)的,不能通過Unsafe.allocateMemory調(diào)用邑商。

幸運的是可以通過Unsafe.getUnsafe()取得Unsafe的實例摄咆。

public class UnsafeTest {

    public static void main(String[] args) {
        Unsafe unsafe = Unsafe.getUnsafe();
        unsafe.allocateMemory(1024);
        unsafe.reallocateMemory(1024, 1024);
        unsafe.freeMemory(1024);
    }
}

此外,也可以通過反射獲取unsafe對象實例

參考:危險代碼:如何使用Unsafe操作內(nèi)存中的Java類和對象

NIO類操作堆外內(nèi)存

用NIO包下的ByteBuffer分配直接內(nèi)存則相對簡單人断。

public class TestDirectByteBuffer {

    public static void main(String[] args) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
    }
}

然而運行時報錯了吭从。

java(51146,0x7000023ed000) malloc: *** error for object 0x400: pointer being realloc'd was not allocated
*** set a breakpoint in malloc_error_break to debug

錯誤信息

參考:JAVA堆外內(nèi)存

然而在小伙伴的電腦上跑這段的代碼是可以成功運行的。


二:堆外內(nèi)存垃圾回收

對于內(nèi)存恶迈,除了關(guān)注怎么分配涩金,還需要關(guān)注如何釋放。

從JAVA出發(fā)蝉绷,習慣性思維是堆外內(nèi)存是否有垃圾回收機制鸭廷。

考慮堆外內(nèi)存的垃圾回收機制,需要了解以下兩個問題:

  1. 堆外內(nèi)存會溢出么熔吗?
  2. 什么時候會觸發(fā)堆外內(nèi)存回收辆床?

問題一

通過修改JVM參數(shù):-XX:MaxDirectMemorySize=40M,將最大堆外內(nèi)存設置為40M桅狠。

既然堆外內(nèi)存有限讼载,則必然會發(fā)生內(nèi)存溢出。

為模擬內(nèi)存溢出中跌,可以設置JVM參數(shù):-XX:+DisableExplicitGC咨堤,禁止代碼中顯式調(diào)用System.gc()。

可以看到出現(xiàn)OOM漩符。

得到的結(jié)論是一喘,堆外內(nèi)存會溢出,并且其垃圾回收依賴于代碼顯式調(diào)用System.gc()。

參考:JAVA堆外內(nèi)存

問題二

關(guān)于堆外內(nèi)存垃圾回收的時機凸克,首先考慮堆外內(nèi)存的分配過程议蟆。

JVM在堆內(nèi)只保存堆外內(nèi)存的引用,用DirectByteBuffer對象來表示萎战。

每個DirectByteBuffer對象在初始化時咐容,都會創(chuàng)建一個對應的Cleaner對象。

這個Cleaner對象會在合適的時候執(zhí)行unsafe.freeMemory(address)蚂维,從而回收這塊堆外內(nèi)存戳粒。

當DirectByteBuffer對象在某次YGC中被回收,只有Cleaner對象知道堆外內(nèi)存的地址虫啥。

當下一次FGC執(zhí)行時蔚约,Cleaner對象會將自身Cleaner鏈表上刪除,并觸發(fā)clean方法清理堆外內(nèi)存孝鹊。

此時炊琉,堆外內(nèi)存將被回收,Cleaner對象也將在下次YGC時被回收又活。

如果JVM一直沒有執(zhí)行FGC的話苔咪,無法觸發(fā)Cleaner對象執(zhí)行clean方法,從而堆外內(nèi)存也一直得不到釋放柳骄。

其實团赏,在ByteBuffer.allocateDirect方式中,會主動調(diào)用System.gc()強制執(zhí)行FGC耐薯。

JVM覺得有需要時舔清,就會真正執(zhí)行GC操作。

顯式調(diào)用

參考:堆外內(nèi)存的回收機制分析—占小狼

三:為什么用堆外內(nèi)存曲初?

堆外內(nèi)存的使用場景非常巧妙体谒。

第三方堆外緩存管理包ohc(off-heap-cache)給出了詳細的解釋。

摘了其中一段臼婆。

When using a very huge number of objects in a very large heap, Virtual machines will suffer from increased GC pressure since it basically has to inspect each and every object whether it can be collected and has to access all memory pages. A cache shall keep a hot set of objects accessible for fast access (e.g. omit disk or network roundtrips). The only solution is to use native memory - and there you will end up with the choice either to use some native code (C/C++) via JNI or use direct memory access.

大概的意思如下:

考慮使用緩存時抒痒,本地緩存是最快速的,但會給虛擬機帶來GC壓力颁褂。

使用硬盤或者分布式緩存的響應時間會比較長故响,這時候「堆外緩存」會是一個比較好的選擇。

參考:OHC - An off-heap-cache — Github

四:如何用堆外內(nèi)存颁独?

在第一章中介紹了兩種分配堆外內(nèi)存的方法彩届,Unsafe和NIO。

對于兩種方法只是停留在分配和回收的階段誓酒,距離真正使用的目標還很遙遠樟蠕。

在第三章中提到堆外內(nèi)存的使用場景之一是緩存。

那是否有一個包,支持分配堆外內(nèi)存坯墨,又支持KV操作寂汇,還無需關(guān)心GC病往。

答案當然是有的捣染。

有一個很知名的包,Ehcache停巷。

Ehcache被廣泛用于Spring耍攘,Hibernate緩存,并且支持堆內(nèi)緩存畔勤,堆外緩存蕾各,磁盤緩存,分布式緩存庆揪。

此外式曲,Ehcache還支持多種緩存策略。

其倉庫坐標如下:

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.4.0</version>
</dependency>

接下來就是寫代碼進行驗證:

public class HelloHeapServiceImpl implements HelloHeapService {

    private static Map<String, InHeapClass> inHeapCache = Maps.newHashMap();

    private static Cache<String, OffHeapClass> offHeapCache;

    static {
        ResourcePools resourcePools = ResourcePoolsBuilder.newResourcePoolsBuilder()
                .offheap(1, MemoryUnit.MB)
                .build();

        CacheConfiguration<String, OffHeapClass> configuration = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(String.class, OffHeapClass.class, resourcePools)
                .build();

        offHeapCache = CacheManagerBuilder.newCacheManagerBuilder()
                .withCache("cacher", configuration)
                .build(true)
                .getCache("cacher", String.class, OffHeapClass.class);


        for (int i = 1; i < 10001; i++) {
            inHeapCache.put("InHeapKey" + i, new InHeapClass("InHeapKey" + i, "InHeapValue" + i));
            offHeapCache.put("OffHeapKey" + i, new OffHeapClass("OffHeapKey" + i, "OffHeapValue" + i));
        }
    }

    @Data
    @AllArgsConstructor
    private static class InHeapClass implements Serializable {
        private String key;
        private String value;
    }

    @Data
    @AllArgsConstructor
    private static class OffHeapClass implements Serializable {
        private String key;
        private String value;
    }

    @Override
    public void helloHeap() {
        System.out.println(JSON.toJSONString(inHeapCache.get("InHeapKey1")));
        System.out.println(JSON.toJSONString(offHeapCache.get("OffHeapKey1")));
        Iterator iterator = offHeapCache.iterator();
        int sum = 0;
        while (iterator.hasNext()) {
            System.out.println(JSON.toJSONString(iterator.next()));
            sum++;
        }
        System.out.println(sum);
    }
}

其中.offheap(1, MemoryUnit.MB)表示分配的是堆外緩存缸榛。

Demo很簡單吝羞,主要做了以下幾步操作:

  1. 新建了一個Map,作為堆內(nèi)緩存内颗。
  2. 用Ehcache新建了一個堆外緩存钧排,緩存大小為1MB。
  3. 在兩種緩存中均澳,都放入10000個對象恨溜。
  4. helloHeap方法做get測試,并統(tǒng)計堆外內(nèi)存數(shù)量找前,驗證先插入的對象是否被淘汰糟袁。

使用Java VisualVM工具Dump一個內(nèi)存鏡像。

Java VisualVM是JDK自帶的工具躺盛。

工具位置如下:

/Library/Java/JavaVirtualMachines/jdk1.7.0_71.jdk/Contents/Home/bin/jvisualvm

也可以使用JProfiler工具项戴。

打開鏡像,堆里有10000個InHeapClass颗品,卻沒有OffHeapClass肯尺,表示堆外緩存中的對象的確沒有占用JVM內(nèi)存。

內(nèi)存鏡像

接著測試helloHeap方法躯枢。

輸出:

{"key":"InHeapKey1","value":"InHeapValue1"}
null
……(此處有大量輸出)
5887

輸出表示堆外內(nèi)存啟用了淘汰機制则吟,插入10000個對象,最后只剩下5887個對象锄蹂。

如果堆外緩存總量不超過最大限制氓仲,則可以順利get到緩存內(nèi)容。

總體而言,使用堆外內(nèi)存可以減少GC的壓力敬扛,從而減少GC對業(yè)務的影響晰洒。


參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市啥箭,隨后出現(xiàn)的幾起案子谍珊,更是在濱河造成了極大的恐慌,老刑警劉巖急侥,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件砌滞,死亡現(xiàn)場離奇詭異,居然都是意外死亡坏怪,警方通過查閱死者的電腦和手機贝润,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來铝宵,“玉大人打掘,你說我怎么就攤上這事∨羟铮” “怎么了尊蚁?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拼岳。 經(jīng)常有香客問我枝誊,道長,這世上最難降的妖魔是什么惜纸? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任叶撒,我火速辦了婚禮,結(jié)果婚禮上耐版,老公的妹妹穿的比我還像新娘祠够。我一直安慰自己,他們只是感情好粪牲,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布古瓤。 她就那樣靜靜地躺著,像睡著了一般腺阳。 火紅的嫁衣襯著肌膚如雪落君。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天亭引,我揣著相機與錄音绎速,去河邊找鬼。 笑死焙蚓,一個胖子當著我的面吹牛纹冤,可吹牛的內(nèi)容都是我干的洒宝。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼萌京,長吁一口氣:“原來是場噩夢啊……” “哼雁歌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起知残,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤靠瞎,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后橡庞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體较坛,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年扒最,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片华嘹。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡吧趣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出耙厚,到底是詐尸還是另有隱情强挫,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布薛躬,位于F島的核電站俯渤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏型宝。R本人自食惡果不足惜八匠,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望趴酣。 院中可真熱鬧梨树,春花似錦、人聲如沸岖寞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仗谆。三九已至指巡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間隶垮,已是汗流浹背藻雪。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留岁疼,地道東北人阔涉。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓缆娃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瑰排。 傳聞我的和親對象是個殘疾皇子贯要,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

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