JVM如何分配和回收堆外記憶體

語言: CN / TW / HK

JVM記憶體模型

在JVM中記憶體被分成兩大塊,分別是堆記憶體和堆外記憶體,堆記憶體就是JVM使用的記憶體,而堆外記憶體就是非JVM使用的記憶體,一般是分配給機器使用的記憶體。

那麼整個記憶體模型如下:

因此在JVM中正常只能分配之際獨有的記憶體即堆記憶體,而我們知道JVM並不建議開發者直接操作堆外記憶體的,因此容易造成記憶體洩漏,並且難以排查,但是在JVM中是可以操作堆外記憶體的並且也可以回收堆外記憶體,但是是一種不建議的方式。

如何分配堆外記憶體

那麼在堆記憶體中如何分配堆外記憶體呢?

在Java中存在兩種方式分配堆外記憶體,分別是ByteBuffer#allocateDirect和Unsafe#allocateMemory。

可能第一個會經常使用到,這是Java NIO提供的一個分配記憶體的類,在做網路開發時會經常使用該方式進行分配記憶體,而第二種方式是Unsafe的方式,我們知道Unsafe是一種不安全的類,該類是提供給開發者操作最底層資料的類,類似C或者C++直接操作記憶體的方式,因此該類並不建議使用,如果使用該類分配記憶體但是沒有及時回收容易造成記憶體洩漏。

第一種方式:ByteBuffer#allocateDirect

該類分配記憶體的實現方式如下:

java //分配10M的記憶體 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);

通過該方式分配堆外記憶體其實最底層還是使用的是Unsafe#allocateMemory進行分配記憶體,ByteBuffer只是對Unsafe做了一層封裝。

第二種方式:Unsafe#allocateMemory

```java public class Test { private static Unsafe unsafe = null;

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    //分配10M的記憶體
    Field getUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    getUnsafe.setAccessible(true);
    unsafe = (Unsafe)getUnsafe.get(null);
    //分配完記憶體返回記憶體的地址
    long address = unsafe.allocateMemory(10 * 1024 * 1024);
}

} ```

該方式中Unsafe類並不能直接被使用,但是可以通過反射的方式使用該類,該類分配記憶體後需要手動回收,不然被分配的記憶體不會被釋放。

如何回收堆外記憶體

說完了如何分配記憶體,那麼繼續瞭解如何回收堆外記憶體。

第一種方式:Unsafe#freeMemory

分配堆外記憶體的兩種方式中,第二種Unsafe的方式其實提供了一個釋放堆外記憶體的實現,實現如下:

```java public class Test { private static Unsafe unsafe = null;

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    //分配10M的記憶體
    Field getUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    getUnsafe.setAccessible(true);
    unsafe = (Unsafe)getUnsafe.get(null);
    //分配完記憶體返回記憶體的地址
    long address = unsafe.allocateMemory(10 * 1024 * 1024);
    //回收分配的堆外記憶體
    unsafe.freeMemory(address);
}

} ```

在Unsafe中提供了freeMemory的實現進行回收堆外記憶體,但是前提是需要知道被分配的堆外記憶體地址才可以實現對應的記憶體回收。

這種回收堆外記憶體的方式其實是開發者自己手動回收,並不是由JVM引起的記憶體回收,那麼JVM如何回收堆外記憶體呢?

第二種方式:JVM回收堆外記憶體

通過ByteBuffer#allocateDirect分配的堆外記憶體在JVM中其實也是存在一定的記憶體佔用的,具體關聯關係如下:

當通過ByteBuffer#allocateDirect分配堆外記憶體後,會將堆外記憶體的地址、大小等資訊通過DirectByteBuffer進行關聯,那麼堆記憶體中就可以關聯到堆外記憶體。

那麼Cleaner又是什麼東西呢?

瞭解Cleaner需要知道JVM中四種引用方式:強引用、弱引用、軟引用、虛引用,Cleaner就是虛引用的實現,上圖中的ReferenceQueue就是一個引用佇列,將需要回收的Cleaner放入到該佇列中,實現邏輯如下:

  1. JVM執行Full GC時會將DirectByteBuffer進行回收,回收之後Clearner就不存在引用關係
  2. 再下一次發生GC時會將Cleaner物件放入ReferenceQueue中,同時將Cleaner從連結串列中移除
  3. 最後呼叫unsafe#freeMemory清除堆外記憶體

那麼可能會存在疑問,為什麼DirectByteBuffer 會被回收呢?

首先DirectByteBuffer 是存在堆記憶體中的物件,那麼既然存在堆記憶體中就會發生GC晉級,即晉升到老年代中,在老年代中就會發生Full GC或者Old GC。

注意點

注意點1:

在實際使用DirectByteBuffer 時要避免把記憶體使用完,但是在實際操作中我們可能不知道堆外記憶體還剩餘多少,因此我們可以在JVM中通過引數控制,通過JVM引數 -XX:MaxDirectMemorySize 指定堆外記憶體的上限大小,當超過指定的記憶體上限大小時,會主動觸發一次Full GC進行回收記憶體。

注意點2:

通過DirectByteBuffer 分配記憶體時,可能會出現分配記憶體不夠的情況,因此JVM如果發現堆外記憶體分配不足時,也會主動發起一次GC,只不過這次GC是通過System.gc() 實現的強制GC,但是在實際生產環境中我們都是通過JVM引數 -XX:+DisableExplicitGC,禁止使用System.gc()的,因此在實際使用過程中一定要注意分配記憶體的情況,避免出現記憶體洩漏。

引用

  • Netty 核心原理剖析與 RPC 實踐
看課的一些總結,如果有誤請指正!