1. 程式人生 > >使用sun.misc.Cleaner或者PhantomReference實現堆外記憶體的自動釋放

使用sun.misc.Cleaner或者PhantomReference實現堆外記憶體的自動釋放

我之前的一篇部落格:System.gc()和-XX:+DisableExplicitGC啟動引數,以及DirectByteBuffer的記憶體釋放文章末尾處:提到java NIO包是通過sun.misc.Cleaner和PhantomReference來實現堆外記憶體的自動釋放的。現在我們來學習下Cleaner和PhantomReference的使用,自己封裝實現堆外記憶體的自動釋放。

sun.misc.Cleaner是JDK內部提供的用來釋放非堆記憶體資源的API。JVM只會幫我們自動釋放堆記憶體資源,但是它提供了回撥機制,通過這個類能方便的釋放系統的其他資源。我們先看下如何使用Cleaner。

package direct;
public class FreeMemoryTask implements Runnable
{
	private long address = 0;

	public FreeMemoryTask(long address)
	{
		this.address = address;
	}

	@Override
	public void run()
	{
		System.out.println("runing FreeMemoryTask");

		if (address == 0)
		{
			System.out.println("already released");
		} else
		{
			GetUsafeInstance.getUnsafeInstance().freeMemory(address);
		}

	}
}
這個實現了Runnable介面的類,功能就是釋放堆外記憶體。這是我們必須要做的事,JVM沒有辦法幫我們做。
public class ObjectInHeapUseCleaner
{
	private long address = 0;

	public ObjectInHeapUseCleaner()
	{
		address = GetUsafeInstance.getUnsafeInstance().allocateMemory(
				2 * 1024 * 1024);
	}

	public static void main(String[] args)
	{
		while (true)
		{
			System.gc();

			ObjectInHeapUseCleaner heap = new ObjectInHeapUseCleaner();

			// 增加heap的虛引用,定義清理的介面FreeMemoryTask
			Cleaner.create(heap, new FreeMemoryTask(heap.address));
		}
	}
}

執行這段程式碼,可以發現程式正常執行,不會出現OOM。

 Cleaner.create()需要2個引數:一個引數:需要監控的堆記憶體物件,第二個引數:程式釋放資源的回撥。當JVM進行GC的時候,如果發現我們監控的物件,不存在強引用了(只被Cleaner物件引用,這是個幽靈引用),就會呼叫第二個引數Runnable.run()方法的邏輯,執行完Runnable.run()的時候(這個時候已經釋放了堆外記憶體),JVM會自動釋放堆記憶體中我們監控的物件。可以看到使用sun.misc.Cleaner很簡單。

接下來我們看下,不使用sun.misc.Cleaner的情況下,如何釋放資源。

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;

public class MyOwnCleaner
{

	private static ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();

	private static Map<Reference<Object>, Runnable> taskMap = new HashMap<Reference<Object>, Runnable>();

	static
	{
		new CleanerThread().start();
	}

	public static void clear(Object heapObject, Runnable task)
	{
		// 當heapObject沒有強引用的時候,reference會自動被JVM加入到引用佇列中
		// 不管使用有人持有reference物件的強引用
		PhantomReference<Object> reference = new PhantomReference<Object>(
				heapObject, refQueue);

		taskMap.put(reference, task);

	}

	// 清理執行緒
	private static class CleanerThread extends Thread
	{
		@Override
		public void run()
		{
			while (true)
			{
				try
				{
					@SuppressWarnings("unchecked")
					Reference<Object> refer = (Reference<Object>) refQueue
							.remove();

					Runnable r = taskMap.remove(refer);
					r.run();
				} catch (InterruptedException e)
				{

				}

			}
		}
	}

}

這裡使用到了PhantomReference和ReferenceQueue,這是JVM內部的物件銷燬機制。當堆中的物件不存在強引用,只存在幽靈引用的時候,JVM會自動將這個物件的幽靈引用加入到與之相關聯的的引用佇列中

private static ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
這個就是引用佇列,JVM會自動將幽靈引用PhantomReference加入到佇列中。也就是說,我們只要輪詢這個佇列,就能夠知道哪些物件即將被JVM回收(這些物件只存在幽靈引用了)。
public static void clear(Object heapObject, Runnable task)
{
	// 當heapObject沒有強引用的時候,reference會自動被JVM加入到引用佇列中
	// 不管使用有人持有reference物件的強引用
	PhantomReference<Object> reference = new PhantomReference<Object>(
			heapObject, refQueue);
	taskMap.put(reference, task);
}
這段程式碼,相當於是我們給堆中的物件heapObject添加了一個監控(註冊了一個幽靈引用)。taskMap記錄幽靈引用和相應的程式碼回收邏輯。之後我們在後臺開啟了一個CleanerThread執行緒,不斷的輪詢引用佇列,一旦發現佇列中有資料(PhantomReference物件),就找出對應的Runnable,呼叫它的run方法,釋放堆物件heapObject中引用的堆外記憶體。測試程式碼如下:
public class Test
{
	private long address = 0;

	public Test()
	{
		address = GetUsafeInstance.getUnsafeInstance().allocateMemory(
				2 * 1024 * 1024);
	}

	public static void main(String[] args)
	{
		while (true)
		{
			Test heap = new Test();

			MyOwnCleaner.clear(heap, new FreeMemoryTask(heap.address));

			System.gc();
		}
	}
}
執行測試程式碼,可以發現也不會報OOM,即正確地實現了堆外記憶體的自動釋放。