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

內(nèi)存分析

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

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

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

當(dāng)緩存中的消息由于各種原因,一直投遞不成功,這些消息會(huì)進(jìn)入老年代。

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

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

有一個(gè)疑問,消息進(jìn)入老年代,出現(xiàn)堆積,為何會(huì)導(dǎo)致YGC時(shí)間過長呢?

按著文章中的敘述,回答這個(gè)問題。

  1. 在YGC階段,涉及到垃圾標(biāo)記的過程,從GCRoot開始標(biāo)記。
  2. 因?yàn)閅GC不涉及到老年代的回收,一旦從GCRoot掃描到引用了老年代對象時(shí),就中斷本次掃描。這樣做可以減少掃描范圍,加速YGC。
  3. 存在被老年代對象引用的年輕代對象,它們沒有被GCRoot直接或者間接引用。
  4. YGC階段中的old-gen scanning即用于掃描被老年代引用的年輕代對象。
  5. old-gen scanning掃描時(shí)間與老年代內(nèi)存占用大小成正比。
  6. 得到結(jié)論,老年代內(nèi)存占用增大會(huì)導(dǎo)致YGC時(shí)間變長。

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

  1. 消息最初緩存到年輕代,會(huì)增加YGC的頻率。
  2. 消息被提升到老年代,會(huì)增加FGC的頻率。
  3. 老年代的消息增長后,會(huì)延長old-gen scanning時(shí)間,從而增加YGC耗時(shí)。

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

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

通過引入自適應(yīng)投遞限流,在實(shí)驗(yàn)室測試環(huán)境下,MsgBroker 在異常場景下的 YGC 耗時(shí)進(jìn)一步從 83ms 降低到 40ms,恢復(fù)了正常的水平。


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

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

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

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

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

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

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

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

sun.misc.Unsafe提供了一組方法來進(jìn)行堆外內(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)造器是私有的,報(bào)錯(cuò)。

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

幸運(yùn)的是可以通過Unsafe.getUnsafe()取得Unsafe的實(shí)例。

public class UnsafeTest {

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

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

參考:危險(xiǎn)代碼:如何使用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);
    }
}

然而運(yùn)行時(shí)報(bào)錯(cuò)了。

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

錯(cuò)誤信息

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

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


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

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

從JAVA出發(fā),習(xí)慣性思維是堆外內(nèi)存是否有垃圾回收機(jī)制。

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

  1. 堆外內(nèi)存會(huì)溢出么?
  2. 什么時(shí)候會(huì)觸發(fā)堆外內(nèi)存回收?

問題一

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

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

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

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

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

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

問題二

關(guān)于堆外內(nèi)存垃圾回收的時(shí)機(jī),首先考慮堆外內(nèi)存的分配過程。

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

每個(gè)DirectByteBuffer對象在初始化時(shí),都會(huì)創(chuàng)建一個(gè)對應(yīng)的Cleaner對象。

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

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

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

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

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

其實(shí),在ByteBuffer.allocateDirect方式中,會(huì)主動(dòng)調(diào)用System.gc()強(qiáng)制執(zhí)行FGC。

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

顯式調(diào)用

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

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

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

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

摘了其中一段。

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.

大概的意思如下:

考慮使用緩存時(shí),本地緩存是最快速的,但會(huì)給虛擬機(jī)帶來GC壓力。

使用硬盤或者分布式緩存的響應(yīng)時(shí)間會(huì)比較長,這時(shí)候「堆外緩存」會(huì)是一個(gè)比較好的選擇。

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

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

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

對于兩種方法只是停留在分配和回收的階段,距離真正使用的目標(biāo)還很遙遠(yuǎn)。

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

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

答案當(dāng)然是有的。

有一個(gè)很知名的包,Ehcache。

Ehcache被廣泛用于Spring,Hibernate緩存,并且支持堆內(nèi)緩存,堆外緩存,磁盤緩存,分布式緩存。

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

其倉庫坐標(biāo)如下:

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

接下來就是寫代碼進(jìn)行驗(yàn)證:

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. 新建了一個(gè)Map,作為堆內(nèi)緩存。
  2. 用Ehcache新建了一個(gè)堆外緩存,緩存大小為1MB。
  3. 在兩種緩存中,都放入10000個(gè)對象。
  4. helloHeap方法做get測試,并統(tǒng)計(jì)堆外內(nèi)存數(shù)量,驗(yàn)證先插入的對象是否被淘汰。

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

Java VisualVM是JDK自帶的工具。

工具位置如下:

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

也可以使用JProfiler工具。

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

內(nèi)存鏡像

接著測試helloHeap方法。

輸出:

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

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

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

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


參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容