天天看點

Unsafe堆外記憶體申請、回收

在nio以前,是沒有光明正大的做法的,唯一的辦法是直接通路Unsafe類。如果你使用Eclipse,預設是不允許通路sun.misc下面的類的,你需要稍微修改一下,給Type Access Rules裡面添加一條所有類都可以通路的規則:

Unsafe堆外記憶體申請、回收

1、使用Unsafe申請記憶體

當我們操作完上面配置後,在代碼裡建立Unsafe對象時:

Unsafe f = Unsafe.getUnsafe();

發現還是被拒絕了,抛出異常:java.lang.SecurityException: Unsafe

正如Unsafe的類注釋中寫道:

Although the class and all methods are public, use of this class is limited because only trusted code can obtain instances of it.

于是,隻能使用反射來做這件事:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe us = (Unsafe) f.get(null);
long id = us.allocateMemory(1024 * 1024 * 1024);      

其中,allocateMemory傳回一個指針,并且其中的資料是未初始化的。如果要釋放這部分記憶體的話,需要調用freeMemory或者reallocateMemory方法。

Unsafe對象提供了一系列put/get方法,例如putByte,但是隻能一個一個byte地put,我不知道這樣會不會影響效率,為什麼不提供一個putByteArray的方法呢?

示例:

import sun.misc.Unsafe;
public class ObjectInHeap {
  private long address = 0;

  private Unsafe unsafe = GetUsafeInstance.getUnsafeInstance();

  public ObjectInHeap() {
    address = unsafe.allocateMemory(2 * 1024 * 1024);
  }

  // Exception in thread "main" java.lang.OutOfMemoryError
  public static void main(String[] args) {
    while (true) {
      ObjectInHeap heap = new ObjectInHeap();
      System.out.println("memory address=" + heap.address);
    }
  }
}      

這段代碼會抛出OutOfMemoryError。這是因為ObjectInHeap對象是在堆記憶體中配置設定的,當該對象被垃圾回收的時候,并不會釋放堆外記憶體,因為使用Unsafe擷取的堆外記憶體,必須由程式顯示的釋放,JVM不會幫助我們做這件事情。由此可見,使用Unsafe是有風險的,很容易導緻記憶體洩露。

2、正确釋放Unsafe配置設定的堆外記憶體

雖然上面的ObjectInHeap存在記憶體洩露,但是這個類的設計是合理的,它很好的封裝了直接記憶體,這個類的調用者感受不到直接記憶體的存在。那怎麼解決ObjectInHeap中的記憶體洩露問題呢?可以覆寫Object.finalize(),當堆中的對象即将被垃圾回收器釋放的時候,會調用該對象的finalize,進而釋放堆外記憶體。

import sun.misc.Unsafe;
public class RevisedObjectInHeap {
  private long address = 0;

  private Unsafe unsafe = GetUsafeInstance.getUnsafeInstance();

  // 讓對象占用堆記憶體,觸發[Full GC
  private byte[] bytes = null;

  public RevisedObjectInHeap() {
    address = unsafe.allocateMemory(2 * 1024 * 1024);
    bytes = new byte[1024 * 1024];
  }

  @Override
  protected void finalize() throws Throwable {
    super.finalize();
    System.out.println("finalize." + bytes.length);
    unsafe.freeMemory(address);
  }

  public static void main(String[] args) {
    while (true) {
      RevisedObjectInHeap heap = new RevisedObjectInHeap();
      System.out.println("memory address=" + heap.address);
    }
  }
}      

我們覆寫了finalize方法,手動釋放配置設定的堆外記憶體。如果堆中的對象被回收,那麼相應的也會釋放占用的堆外記憶體。這裡有一點需要注意下:

// 讓對象占用堆記憶體,觸發[Full GC

private byte[] bytes = null;

這行代碼主要目的是為了觸發堆記憶體的垃圾回收行為,順帶執行對象的finalize釋放堆外記憶體。如果沒有這行代碼或者是配置設定的位元組數組比較小,程式運作一段時間後還是會報OutOfMemoryError。這是因為每當建立1個RevisedObjectInHeap對象的時候,占用的堆記憶體很小(就幾十個位元組左右),但是卻需要占用2M的堆外記憶體。這樣堆記憶體還很充足(這種情況下不會執行堆記憶體的垃圾回收),但是堆外記憶體已經不足,是以就不會報OutOfMemoryError。

參考: