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