使用sun.misc.Cleaner或者PhantomReference實現堆外記憶體的自動釋放
阿新 • • 發佈:2019-02-13
我之前的一篇部落格:System.gc()和-XX:+DisableExplicitGC啟動引數,以及DirectByteBuffer的記憶體釋放文章末尾處:提到java NIO包是通過sun.misc.Cleaner和PhantomReference來實現堆外記憶體的自動釋放的。現在我們來學習下Cleaner和PhantomReference的使用,自己封裝實現堆外記憶體的自動釋放。
sun.misc.Cleaner是JDK內部提供的用來釋放非堆記憶體資源的API。JVM只會幫我們自動釋放堆記憶體資源,但是它提供了回撥機制,通過這個類能方便的釋放系統的其他資源。我們先看下如何使用Cleaner。
這個實現了Runnable介面的類,功能就是釋放堆外記憶體。這是我們必須要做的事,JVM沒有辦法幫我們做。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); } } }
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,即正確地實現了堆外記憶體的自動釋放。