天天看點

Java虛拟機之強引用、軟引用、弱引用、虛引用詳解

背景

在JDK 1.2以前,Java中的引用定義很傳統:如果reference類型的資料中存儲的數值代表的是另一塊記憶體的起始位址,就稱這塊記憶體代表着一個引用。這種定義很純粹,但是太過狹隘,一個對象在這種定義下,隻有被引用或者沒有被引用這兩種狀态,對于如何描述一些“食之無味,棄之可惜”的對象就顯得無能為力。我們對于這樣一類對象需要一種新的引用方式:當記憶體空間還足夠時,則能保留在記憶體之中;如果記憶體空間在進行垃圾回收後還是非常緊張,則可以抛棄這些對象。很多系統的緩存功能都是符合這樣的應用場景。

是以,JDK 1.2對引用的概念進行了擴充,将引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種的引用強度依次逐漸減弱。

強引用

強引用是指在程式代碼中普遍存在的,類似Object obj = new Object()這類的引用,隻要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象。

軟引用

軟引用用來描述一些還有用但并非必需的對象。對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。在JDK 1.2之後,提供了SoftReference類來實作軟引用。

應用場景:軟引用通常用來實作記憶體敏感的緩存。如果記憶體空間還有剩餘,就可以暫時保留緩存,當記憶體不足時,就可以将緩存清除,這樣就保證了使用緩存的同時,不會耗盡記憶體。

弱引用

弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次垃圾回收之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。在JDK 1.2之後,提供了WeakReference類來實作弱引用。

應用場景:弱應用同樣可用于記憶體敏感的緩存。與軟引用不同的是,在記憶體空間還足夠時,軟引用的對象不會被回收,但是,弱引用的對象有可能會被回收,其存活時間相比于弱引用更短一點。

虛引用

虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象執行個體。為一個對象設定虛引用關聯的唯一目的就是在這個對象被垃圾回收器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實作虛引用。

應用場景:可以用虛引用來跟蹤對象被垃圾回收器回收的活動,當一個虛引用關聯的對象被垃圾收集器回收之前會收到一條系統通知。

軟引用、弱引用和虛引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果它們所引用的對象被垃圾回收,Java虛拟機就會把這個引用加入到與之關聯的引用隊列中。采用軟引用、弱引用和虛引用可以預防Java虛拟機抛出OOM異常以及記憶體洩漏的問題。

軟引用、弱引用和虛引用存在的必要性

在Java程式中,如果我們想回收一個對象,可以把對象引用置為null,這樣,在下次垃圾回收之前,就可以回收該對象,如下所示:

Object object = new Object();
object = null;           

但是手動置為null這種方式太繁瑣,而且我們編寫程式時也不能一直保證對引用的置空操作是正确的,這種方式完全違背了Java虛拟機自動回收垃圾的概念。同時,采用該種方式會存在以下問題:

對于需要系統緩存的Java程式,在程式運作期間,緩存内容就不能被垃圾回收器回收,如果我們已經不再需要某項緩存,那将其置為null,是不是就可以了呢?

答案是不行的,若緩存使用HashMap<Product, Integer>來實作,其中,key代表一種商品緩存資訊,value為商品剩餘數目,當商品售空時,我們将指向Product商品的引用(假設為productRef)productRef置為null,然而垃圾回收器并不會回收Product對象,因為在HashMap的Node結點中,仍然保留着一個對Product對象的引用,若是想要回收該對象,必須還得将該條目從HashMap中删除,否則會産生記憶體洩漏問題。這同樣違背了Java虛拟機自動回收垃圾的概念。

對于上述的問題,我們都可以通過軟引用和弱引用解決。對于使用單個引用的對象,示例代碼如下所示:

public class ReferenceTest1 {
	// 循環檢測引用隊列是否有值
	static class CheckReferenceQueueThread extends Thread {
		private ReferenceQueue<Object> referenceQueue;
		
		public CheckReferenceQueueThread(ReferenceQueue<Object> referenceQueue) {
			this.referenceQueue = referenceQueue;
		}
		
		@Override
		public void run() {
			super.run();
			
			while (!Thread.interrupted()) {
				Reference<? extends Object> reference = referenceQueue.poll();
				if (reference != null) {
					System.out.println("弱引用對象已被回收,已被加入到引用隊列中");
				}
			}
		}
	}
	
	// 循環擷取弱引用指向的對象
	static class GetValueThread extends Thread {
		private WeakReference<Object> weakReference;
		
		public GetValueThread(WeakReference<Object> weakReference) {
			this.weakReference = weakReference;
		}
		
		@Override
		public void run() {
			super.run();
			
			while (!Thread.interrupted()) {
				Object object = weakReference.get();
				if (object == null) {
					System.out.println("弱引用對象已被回收,重新為弱引用指派");
					weakReference = new WeakReference<>(new Object());
				}
			}
		}
	}
	
	private static final ReferenceQueue<Object> REFERENCE_QUEUE = new ReferenceQueue<>();

	public static void main(String[] args) throws InterruptedException {
		GetValueThread getValueThread = new GetValueThread(getWeakReference());
		CheckReferenceQueueThread checkReferenceQueueThread = new CheckReferenceQueueThread(REFERENCE_QUEUE);
		
		getValueThread.start();
		checkReferenceQueueThread.start();
		
		Thread.sleep(1000);
		System.gc();
		Thread.sleep(1000);
		
		getValueThread.interrupt();
		checkReferenceQueueThread.interrupt();
	}
	
	private static WeakReference<Object> getWeakReference() {
		Object object = new Object();
		WeakReference<Object> weakReference = new WeakReference<Object>(object, REFERENCE_QUEUE);
		return weakReference;
	}
}           

運作結果為:

弱引用對象已被回收,重新為弱引用指派
弱引用對象已被回收,已被加入到引用隊列中           

可以看到,被引用的對象可以自動被垃圾回收器回收,若此時我們仍需要使用此對象,則可以重新生成對象。如果是緩存的話,可以對緩存資訊進行重新讀取。

同樣,對于使用Map引用的對象,Java類庫為我們提供了WeakHashMap類,使用和這個類,它的鍵自然就是弱引用對象,無需我們再手動包裝原始對象。當一個鍵對象被垃圾回收器回收時,那麼相應的值對象的引用會從Map中删除。WeakHashMap能夠節約存儲空間,可用來緩存那些非必須存在的資料。

相關部落格

Java集合之WeakHashMap詳解

參考資料

周志明:《深入了解Java虛拟機》