java中使用堆外記憶體,關於記憶體回收需要注意的事和沒有解決的遺留問題(等大神解答)
使用ByteBuffer分配本地記憶體則非常簡單,直接ByteBuffer.allocateDirect(10 * 1024 * 1024)即可。
C語言的記憶體分配和釋放函式malloc/free,必須要一一對應,否則就會出現記憶體洩露或者是野指標的非法訪問。java中我們需要手動釋放獲取的堆外記憶體嗎?
1、首先我們看下NIO中提供的ByteBuffer
import java.nio.ByteBuffer; public class TestDirectByteBuffer { // -verbose:gc -XX:+PrintGCDetails -XX:MaxDirectMemorySize=40M public static void main(String[] args) throws Exception { while (true) { ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024); } } }
我們將最大堆外記憶體設定成40M,執行這段程式碼會發現:程式可以一直執行下去,不會報OutOfMemoryError。如果使用了-verbose:gc -XX:+PrintGCDetails,會發現程式頻繁的進行垃圾回收活動。於是我們可以得出結論:ByteBuffer.allocateDirect分配的堆外記憶體不需要我們手動釋放,而且ByteBuffer中也沒有提供手動釋放的API。也即是說,使用ByteBuffer不用擔心堆外記憶體的釋放問題,除非堆記憶體中的ByteBuffer物件由於錯誤編碼而出現記憶體洩露。
2、接下來我們看下直接在方法體中使用Unsafe的效果
這段程式會報OutOfMemoryError錯誤,也就是說allocateMemory和freeMemory,相當於C語音中的malloc和free,必須手動釋放分配的記憶體。import sun.misc.Unsafe; public class TestUnsafeMemo { // -XX:MaxDirectMemorySize=40M public static void main(String[] args) throws Exception { Unsafe unsafe = GetUsafeInstance.getUnsafeInstance(); while (true) { long pointer = unsafe.allocateMemory(1024 * 1024 * 20); System.out.println(unsafe.getByte(pointer + 1)); // 如果不釋放記憶體,執行一段時間會報錯java.lang.OutOfMemoryError // unsafe.freeMemory(pointer); } } }
3、類似於ByteBuffer,將Unsafe分配記憶體封裝到一個類中
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是有風險的,很容易導致記憶體洩露。4、正確釋放Unsafe分配的堆外記憶體
雖然第3種情況的ObjectInHeap存在記憶體洩露,但是這個類的設計是合理的,它很好的封裝了直接記憶體,這個類的呼叫者感受不到直接記憶體的存在。那怎麼解決ObjectInHeap中的記憶體洩露問題呢?可以覆寫Object.finalize(),當堆中的物件即將被垃圾回收器釋放的時候,會呼叫該物件的finalize。由於JVM只會幫助我們管理記憶體資源,不會幫助我們管理資料庫連線,檔案控制代碼等資源,所以我們需要在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。
雖然改進後的RevisedObjectInHeap不會有堆外記憶體洩露,但是這種解決方法卻無端地浪費了堆記憶體。簡單的看了下ByteBuffer的原始碼,它內部分配堆外記憶體也是通過unsafe.allocateMemory()實現的。那ByteBuffer又是怎麼實現的堆外記憶體釋放呢?難道也是通過第4種類似RevisedObjectInHeap的做法嗎?歡迎大神指點迷津啊!