爲了知足對不一樣狀況的垃圾回收需求,從Java從版本1.2開始,引入了4種引用類型(實際上是額外增長了三種)的概念。本文將詳細介紹這四種引用。html
Java中的4中引用類型分別爲強引用(String Reference),軟引用(Soft Reference),弱引用(Weak Reference)和虛引用(Phantom Reference)。java
> - 強引用:Java中的引用,默認都是強引用。好比new一個對象,對它的引用就是強引用。對於被強引用指向的對象,就算JVM內存不足OOM,也不會去回收它們。 > - 軟引用:若一個對象只被軟引用所引用,那麼它將在JVM內存不足的時候被回收,即若是JVM內存足夠,則軟引用所指向的對象不會被垃圾回收(其實這個說法也不夠準確,具體緣由後面再說)。根據這個性質,軟引用很適合作內存緩存:既能提升查詢效率,也不會形成內存泄漏。 > - 弱引用:若一個對象只被弱引用所引用,那麼它將在下一次GC中被回收掉。如ThreadLocal和WeakHashMap中都使用了弱引用,防止內存泄漏。 > - 虛引用:虛引用是四種引用中最弱的一種引用。咱們永遠沒法從虛引用中拿到對象,被虛引用引用的對象就跟不存在同樣。虛引用通常用來跟蹤垃圾回收狀況,或者能夠完成垃圾收集器以外的一些定製化操做。Java NIO中的堆外內存(DirectByteBuffer)由於不受GC的管理,這些內存的清理就是經過虛引用來完成的。緩存
引用隊列(Reference Queue)是一個鏈表,顧名思義,存放的是引用對象(Reference對象)的隊列。 軟引用與弱引用能夠和一個引用隊列配合使用,當引用所指向的對象被垃圾回收以後,該引用對象自己會被添加到與之關聯的引用隊列中,從而方便後續一些跟蹤或者額外的清理操做。 由於沒法從虛引用中拿到目標對象,虛引用必須和一個引用隊列配合使用。ide
設置JVM的啓動參數爲 > -Xms10m -Xmx10mthis
public class ReferenceTest { private static int _1MB = 1024 * 1024; private static int _1KB = 1024; public static void main(String[] args) throws InterruptedException { // 引用隊列,存放Reference對象 ReferenceQueue queue = new ReferenceQueue(); // 定義四種引用對象,強/弱/虛引用爲1kb,軟引用爲1mb Byte[] strong = new Byte[_1KB]; SoftReference<byte[]> soft = new SoftReference<>(new Byte[_1MB], queue); WeakReference<byte[]> weak = new WeakReference<>(new Byte[_1KB], queue); PhantomReference<byte[]> phantom = new PhantomReference<>(new Byte[_1KB], queue); Reference<string> collectedReference; // 初始狀態 System.out.println("Init: Strong Reference is " + strong); System.out.println("Init: Soft Reference is " + soft.get()); System.out.println("Init: Weak Reference is " + weak.get()); System.out.println("Init: Phantom Reference is " + phantom.get()); do { collectedReference = queue.poll(); System.out.println("Init: Reference In Queue is " + collectedReference); } while (collectedReference != null); System.out.println("********************"); // 第一次手動觸發GC System.gc(); // 停100ms保證垃圾回收已經執行 Thread.sleep(100); System.out.println("After GC: Strong Reference is " + strong); System.out.println("After GC: Soft Reference is " + soft.get()); System.out.println("After GC: Weak Reference is " + weak.get()); System.out.println("After GC: Phantom Reference is " + phantom.get()); do { collectedReference = queue.poll(); System.out.println("After GC: Reference In Queue is " + collectedReference); } while (collectedReference != null); System.out.println("********************"); // 再分配1M的內存,以模擬OOM的狀況 Byte[] newByte = new Byte[_1MB]; System.out.println("After OOM: Strong Reference is " + strong); System.out.println("After OOM: Soft Reference is " + soft.get()); System.out.println("After OOM: Weak Reference is " + weak.get()); System.out.println("After OOM: Phantom Reference is " + phantom.get()); do { collectedReference = queue.poll(); System.out.println("After OOM: Reference In Queue is " + collectedReference); } while (collectedReference != null); } }
上述代碼的輸出結果爲:線程
Init: Strong Reference is [Ljava.lang.Byte;@74a14482 Init: Soft Reference is [Ljava.lang.Byte;@1540e19d Init: Weak Reference is [Ljava.lang.Byte;@677327b6 Init: Phantom Reference is null Init: Reference In Queue is null ******************** After GC: Strong Reference is [Ljava.lang.Byte;@74a14482 After GC: Soft Reference is [Ljava.lang.Byte;@1540e19d After GC: Weak Reference is null After GC: Phantom Reference is null After GC: Reference In Queue is java.lang.ref.WeakReference@14ae5a5 After GC: Reference In Queue is java.lang.ref.PhantomReference@7f31245a After GC: Reference In Queue is null ******************** After OOM: Strong Reference is [Ljava.lang.Byte;@74a14482 After OOM: Soft Reference is null After OOM: Weak Reference is null After OOM: Phantom Reference is null After OOM: Reference In Queue is java.lang.ref.SoftReference@6d6f6e28 After OOM: Reference In Queue is null
如下是引用類的UML圖code
弱引用,軟引用和虛引用都繼承自Reference類,咱們從Reference類看起htm
// 此Reference對象可能會有四種狀態:active, pending, enqueued, inactive // avtive: 新建立的對象狀態是active // pending: 當Reference所指向的對象不可達,而且Reference與一個引用隊列關聯,那麼垃圾收集器 // 會將Reference標記爲pending,而且會將之加到pending隊列裏面 // enqueued: 當Reference從pending隊列中,移到引用隊列中以後,就是enqueued狀態 // inactive: 若是Reference所指向的對象不可達,而且Reference沒有與引用隊列關聯,Reference // 從引用隊列移除以後,變爲inactive狀態。inactive就是最終狀態 public abstract class Reference<t> { // 該對象就是Reference所指向的對象,垃圾收集器會對此對象作特殊處理。 private T referent; /* Treated specially by GC */ // Reference相關聯的引用隊列 volatile ReferenceQueue<!--? super T--> queue; // 當Reference是active時,next爲null // 當該Reference處於引用隊列中時,next指向隊列中的下一個Reference // 其餘狀況next指向this,即本身 // 垃圾收集器只需判斷next是否是爲null,來看是否須要對此Reference作特殊處理 volatile Reference next; // 當Reference在pending隊列中時,該值指向下一個隊列中Reference對象 // 另外垃圾收集器在GC過程當中,也會用此對象作標記 transient private Reference<t> discovered; /* used by VM */ // 鎖對象 static private class Lock { } private static Lock lock = new Lock(); // pending隊列,這裏的pending是pending鏈表的隊首元素,通常與上面的discovered變量一塊兒使用 private static Reference<object> pending = null; // 獲取Reference指向的對象。默認返回referent對象 public T get() { return this.referent; } }
Reference類跟垃圾收集器緊密關聯,其狀態變化以下圖所示:對象
上述步驟大多數都是由GC線程來完成,其中Pending到Enqueued是用戶線程來作的。Reference類中定義了一個子類ReferenceHandler,專門用來處理Pending狀態的Reference。咱們來看看它具體作了什麼。blog
public abstract class Reference<t> { // 靜態塊,主要邏輯是啓動ReferenceHandler線程 static { // 建立ReferenceHandler線程 ThreadGroup tg = Thread.currentThread().getThreadGroup(); for (ThreadGroup tgn = tg; tgn != null; tg = tgn, tgn = tg.getParent()); Thread handler = new ReferenceHandler(tg, "Reference Handler"); // 設置成守護線程,最高優先級,並啓動 handler.setPriority(Thread.MAX_PRIORITY); handler.setDaemon(true); handler.start(); // 訪問控制 SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() { @Override public boolean tryHandlePendingReference() { return tryHandlePending(false); } }); } // 內部類ReferenceHandler,用來處理Pending狀態的Reference private static class ReferenceHandler extends Thread { private static void ensureClassInitialized(Class<!--?--> clazz) { try { Class.forName(clazz.getName(), true, clazz.getClassLoader()); } catch (ClassNotFoundException e) { throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e); } } // 靜態塊,確保InterruptedException和Cleaner已經被ClassLoader加載 // 由於後面會用到這兩個類 static { ensureClassInitialized(InterruptedException.class); ensureClassInitialized(Cleaner.class); } ReferenceHandler(ThreadGroup g, String name) { super(g, name); } public void run() { // 死循環調用tryHandlePending方法 while (true) { tryHandlePending(true); } } } }
Reference類在加載進JVM的時候,會啓動ReferenceHandler線程,並將它設成最高優先級的守護線程,不斷循環調用tryHandlePending方法。 接下來看tryHandlePending方法:
// waitForNotify默認是true。 static boolean tryHandlePending(boolean waitForNotify) { Reference<object> r; Cleaner c; try { // 須要在同步塊中進行 synchronized (lock) { // 判斷pending隊列是否爲空,pending是隊首元素 if (pending != null) { // 取到pending隊列隊首元素,賦值給r r = pending; // Cleaner類是Java NIO中專門用來清理堆外內存(DirectByteBufer)的類,這裏對它作了特殊處理 // 當沒有其餘引用指向堆外內存時,與之關聯的Cleaner會被加到pending隊列中 // 若是該Reference是Cleaner實例,那麼取到該Cleaner,後續能夠作一些清理操做。 c = r instanceof Cleaner ? (Cleaner) r : null; // r.discovered就是下一個元素 // 如下操做即爲將隊首元素從pending隊列移除 pending = r.discovered; r.discovered = null; } else { // 若是pending隊列爲空,則釋放鎖等待 // 當有Reference添加到pending隊列中時,ReferenceHandler線程會今後處被喚醒 if (waitForNotify) { lock.wait(); } return waitForNotify; } } } catch (OutOfMemoryError x) { // OOM時,讓出cpu Thread.yield(); return true; } catch (InterruptedException x) { return true; } // 給Cleaner的特殊處理,調用clean()方法,以釋放與之關聯的堆外內存 if (c != null) { c.clean(); return true; } // 此處,將此Reference加入到與之關聯的引用隊列 ReferenceQueue<!--? super Object--> q = r.queue; if (q != ReferenceQueue.NULL) q.enqueue(r); return true; }
看到這裏,豁然開朗。ReferenceHandler線程專門用來處理pending狀態的Reference,跟GC線程組成相似生產者消費者的關係。當pending隊列爲空,則等待;當Reference關聯的對象被回收,Reference被加入到pending隊列中以後,ReferenceHandler線程會被喚醒來處理pending的Reference,主要作三件事:
引用隊列比較簡單,能夠直接理解爲一個存放Reference的鏈表,在此再也不費筆墨。
// 灰常簡單,只重寫了一個構造方法,一個get方法 public class PhantomReference<t> extends Reference<t> { // get方法永遠返回null public T get() { return null; } // 只提供了一個包含ReferenceQueue的構造方法,說明它必須和引用隊列一塊兒使用 public PhantomReference(T referent, ReferenceQueue<!--? super T--> q) { super(referent, q); } }
通常狀況下虛引用使用得比較少,最爲人所熟知的就是PhantomReference的子類Cleaner了,它用來清理NIO中的堆外內存。有機會能夠專門寫篇文章來說講它。
// 更加簡單,只重寫了兩個構造方法 public class WeakReference<t> extends Reference<t> { public WeakReference(T referent) { super(referent); } public WeakReference(T referent, ReferenceQueue<!--? super T--> q) { super(referent, q); } }
太過簡單,不作額外講解。
// 相比WeakReference,它增長了兩個時間戳,clock和timestamp // 這兩個參數是實現他們內存回收上區別的關鍵 public class SoftReference<t> extends Reference<t> { // 每次GC以後,若該引用指向的對象沒有被回收,則垃圾收集器會將clock更新成當前時間 static private long clock; // 每次調用get方法的時候,會更新該時間戳爲clock值 // 因此該值保存的是上一次(最近一次)GC的時間戳 private long timestamp; public SoftReference(T referent) { super(referent); this.timestamp = clock; } public SoftReference(T referent, ReferenceQueue<!--? super T--> q) { super(referent, q); this.timestamp = clock; } // 每次調用,更新timestamp的值,使之等於clock的值,即最近一次gc的時間 public T get() { T o = super.get(); if (o != null && this.timestamp != clock) this.timestamp = clock; return o; } }
SoftReference除了多了兩個時間戳以外,跟WeakReference幾乎沒有區別,它是如何作到在內存不足時被回收這件事的呢?其實這是垃圾收集器乾的活。垃圾收集器回收SoftReference所指向的對象,會看兩個維度:
而具體何時回收SoftReference所指向的對象呢,能夠參考以下公式: > interval <= free_heap * ms_per_mb
其中interval爲上一次GC與當前時間的差值,以毫秒爲單位;free_heap爲當前JVM中剩餘的堆空間大小,以MB爲單位;ms_per_mb能夠理解爲一個常數,即每兆空閒空間可維持的SoftReference的對象生存的時長,默認爲1000,能夠經過JVM參數*-XX:SoftRefLRUPolicyMSPerMB設置。 若是上述表達式返回true,則清理SoftReference所指向的對象,並將該SoftReference加入到pending*隊列中;不然不作處理。因此說在JVM內存不足的時候回收軟引用這個說法不是很是準確,只是個經驗說法,軟引用的回收,還跟它存活的時間有關,甚至跟JVM參數設置(-XX:SoftRefLRUPolicyMSPerMB)都有關係!
How Hotspot Clear Softreference</t></t></t></t></t></t></object></t></object></t></t></string></byte[]></byte[]></byte[]>