1. 程式人生 > >java中使用堆外記憶體,關於記憶體回收需要注意的事和沒有解決的遺留問題(等大神解答)

java中使用堆外記憶體,關於記憶體回收需要注意的事和沒有解決的遺留問題(等大神解答)

JVM可以使用的記憶體分外2種:堆記憶體和堆外記憶體,堆記憶體完全由JVM負責分配和釋放,如果程式沒有缺陷程式碼導致記憶體洩露,那麼就不會遇到java.lang.OutOfMemoryError這個錯誤。使用堆外記憶體,就是為了能直接分配和釋放記憶體,提高效率。JDK5.0之後,程式碼中能直接操作本地記憶體的方式有2種:使用未公開的Unsafe和NIO包下ByteBuffer。

使用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的效果

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);
		}
	}

}
這段程式會報OutOfMemoryError錯誤,也就是說allocateMemory和freeMemory,相當於C語音中的malloc和free,必須手動釋放分配的記憶體

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的做法嗎?歡迎大神指點迷津啊!