在Java語言中,引用是指,某一個數據,代表的是另外一塊內存的的起始地址,那麼我們就稱這個數據為引用。
在JVM中,GC回收的大致准則,是認定如果不能從根節點,根據引用的不斷傳遞,最終指向到一塊內存區域,我們就將這塊內存區域回收掉。但是這樣的回收原則未免太過粗暴。有些時候,內存的使用並不緊張,我們並不希望GC那麼勤勞的、快速的回收掉內存。反而有時候希望數據可以在內存中盡可能的保留長一會,待到虛擬機內存吃緊的時候,再來清理掉他。因此從JDK1.2之後,引用的類型變的多樣化,從而更好的適應編碼的需要。
下面次來介紹下四種引用:
1、強引用 Strong Reference
這是Java程序中,最普遍的一種引用。
程序創建一個對象,並且把這個對象賦值給一個引用變量,我們就稱這個引用變量為強引用。很多書上說,強引用是不會被GC回收掉的,個人覺得這話是需要背景的:即強引用變量所處的位置,一定是在GC回收,所判定的Root節點能夠依次傳遞到的引用,如果出現孤立的循環引用。那麼即使對象中,存在強引用,也一定是會被回收掉的。其次需要強調的就是強引用不被回收,一定要(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )處在強引用所在的作用域中,如方法棧已經彈出,那麼棧幀中的局部變量表中的變量就會被回收,其中存在的強引用的指向關系也會被解除。當然叨叨這麼多,只是想說,強引用是否被回收,一定要看具體的情況,而不能一概而論。
筆者認為,引用,就類似於生活中對物品的持有狀態,如果一件物品,對我們至關重要,是必不可少的,無論如何打掃衛生我們都不可能會清理掉他們,那麼這種關系狀態,我們就認為是強引用。
2、軟引用 Soft Reference
當一個對象的引用關系一直保留,GC就不會清理掉這個對象,我們稱之為強引用。在平常的開發中,我們還希望有這樣一種引用狀態:只要內存夠用,即使GC進行回收,我們仍然會一直保留,反之倘若內存不夠用,那麼下次GC回收時,就會處理掉強引用所指向的對象。
強引用可以理解為GC永遠不會強制刪除的引用,而軟引用,則可以理解為,家中存放的可有可無的物件,比如可有可無的廢棄的家具、電腦中已經不會再使用的軟件、手機上保存的可能不會再翻閱浏覽的信息、照片、視頻。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )對於這些東西,只要家中仍然有剩余的空間,手機中仍然有足夠的硬盤空間,大部分人都會一直保留,直到手機廢棄、家中拆遷。但是倘若,手機的硬盤空間開始吃緊、家中沒有剩余的空間可供使用,很多人就會選擇一次性的,把這些沒用的東西全部都處理掉,盡管這些東西可能在以前的清理過程中,一直被保留。
Java的這種設計,正是為了模擬類似於生活中,對於雞肋物件的關系。如果保存空間足夠,那麼久保留該物件,如果保存空間不足,那麼才開始清理。
3、弱引用 Weak Reference
軟引用讓Jvm的內存管理,擁有彈性,可以根據使用情況動態的調整要回收的對象。弱引用與軟引用的性質類似。不同之處在於,對於弱引用指向的對象,無論內存是否夠用,下次GC回收時,都會回收掉該塊內存。這就像我們平常打掃衛生,有些東西一直在使用,但是倘若要打掃衛生了,就一定會處理掉這些物品。即使房間內仍然有足夠的空間,可以保留這些物品。
對於軟引用和弱引用,在使用時,不可以再像原有強引用一般,直接給引用變量賦值,否則強引用關系會再次建立。這裡則需要依賴java.lang.ref包下的幾個類,使用方法可以參考如下代碼:
1 import java.lang.ref.SoftReference; 2 import java.lang.ref.WeakReference; 3 4 public class RefLearn 5 { 6 7 private static WeakReference<RefLearn> weakRef0; 8 private static WeakReference<RefLearn> weakRef1; 9 10 public static void main(String arg[]) throws InterruptedException 11 { 12 RefLearn refLearn = new RefLearn(); 13 refLearn.init(); 14 } 15 16 private void init() throws InterruptedException 17 { 18 RefLearn obj = new RefLearn(); 19 SoftReference<RefLearn> softRef = new SoftReference<RefLearn>(obj);// 將實例傳進來 20 obj = null;// 切斷原有的強引用,一般使用時,直接在構造函數中new 即可 21 weakRef0 = new WeakReference<RefLearn>(new RefLearn()); 22 weakRef1 = new WeakReference<RefLearn>(new RefLearn()); 23 softRef.get().doNothing(); 24 if (weakRef0.get() != null) 25 { 26 // System.gc(); 27 // Thread.sleep(2000); 28 Thread.sleep(1000); 29 weakRef0.get().doSomething(); 30 } 31 // obj = weakRef.get();// 這裡會再次建立強引用,會阻止回收 32 } 33 34 private void doNothing() 35 { 36 37 } 38 39 private void doSomething() 40 { 41 int i = 0; 42 while (true) 43 { 44 try 45 { 46 System.gc(); 47 Thread.sleep(1000); 48 boolean relate0 = weakRef0.get() == null; 49 boolean relate1 = weakRef1.get() == null; 50 System.out.println(relate0 + " " + relate1 + " " + i++); 51 } 52 catch (InterruptedException e) 53 { 54 } 55 } 56 } 57 }
代碼中通過引用類的get()方法,可以獲取到非強引用所指向的變量,同時使用它們。
通過上述代碼,我們會發現兩個問題
問題一
如果內存吃緊,那麼是所有軟引用都被回收,還是只回收盡可能少的軟引用?
答案是後者,建立軟引用後,引用對象會被打一個時間戳,標記該引用當前所處的GC時間,也就是這兩個時間:
1 /** 2 * Timestamp clock, updated by the garbage collector 3 */ 4 static private long clock; 5 /** 6 * Timestamp updated by each invocation of the get method. The VM may use 7 * this field when selecting soft references to be cleared, but it is not 8 * required to do so. 9 */ 10 private long timestamp;
當該軟引用每次被調用get時,都會修改該引用的時間戳,來標識該引用指向變量的最後調用時間。GC會在回收過程中,優先回收一直不被使用的軟引用。
問題二(這是一道大題,有兩個小問(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ ))
(1)在判斷get()的取值是否為null後,再次使用時,如果這個期間,內存被回收了,怎麼辦?
(2)如果在執行弱引用執行的方法時,內存是否會被回收掉?
問題(1)的情況顯然會拋出空引用異常
因此在使用軟引用和弱引用時,務必要注意空引用異常。
問題(2)的情況我們可以看示例代碼的運行結果:
通過實現可以知道,如果正在執行一個弱引用所指向內存的方法,那麼這個虛弱引用是可以逃過這段時間內GC的回收的。其實想想也該明白,方法參數其實默認有this變量作為參數(這個以後會說)。同時方法棧幀中的局部變量表可能也會指向於堆中的其他變量,倘若直接回收,未來可能會指向無訪問權限的內存區域,導致出現內存安全問題。
4、虛引用 Phantom Reference
Phantom ['fæntəm]
adj. 幽靈的;幻覺的;有名無實的
通過名稱我們就可以發現,這個引用的強度屬於微乎其微的。事實也的確如此。
我們幾乎已經無法通過虛引用,查找到任何其所指向實例的內部信息。唯一可以獲得的僅僅是該對象是否已經被回收(即是否已經經過一次GC過程)。如果非要用虛引用與現實生活中的某種聯系相類比的話,個人覺得有點像已經丟棄到回收站中的文件,當我們打開回收站時,是不能直接使用這些軟件的,只能判斷這些軟件有沒有被回收掉,而如果真正想再次使用這些軟件的話,需要再次建立關系性更強的引用才可以。
虛引用與上述的其他三個引用有比較大的區別。
我們先來看一段代碼
1 import java.lang.ref.PhantomReference; 2 import java.lang.ref.ReferenceQueue; 3 4 public class PhantomReferenceLearn 5 { 6 public void init() throws InterruptedException 7 { 8 ReferenceQueue<PhantomReferenceLearn> Refqueue = new ReferenceQueue<PhantomReferenceLearn>(); 9 PhantomReference<PhantomReferenceLearn> pr = new PhantomReference<PhantomReferenceLearn>( 10 new PhantomReferenceLearn(), Refqueue); 11 System.gc(); 12 Thread.sleep(1000); 13 boolean isClear = Refqueue.poll() == pr; 14 System.out.println(isClear); 15 } 16 }
與軟引用和弱引用不同的是,虛引用的構造函數只能同時伴隨著一個引用隊列來構造。當虛引用回收時,引用會被加載到構造時綁定的引用隊列中,可以通過出隊的方式來查看引用是否已經被回收。ps.與虛引用類似,軟引用和弱引用也有類似的用法,不同的地方是虛引用只能這樣使用。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )
對於虛引用的使用還有以下幾點要介紹
1)、由於虛引用在定義時,就已經明確其不可以通過引用關系,取出指向內存中的數據,因此盡管虛引用實例中也存有get()方法,但實際上一定返回的是null值,這點是在JDK代碼中寫死的:
注釋的意思是返回一個對象的引用,但是由於虛引用是始終不可達的,因此始終返回null
1 /** 2 * Returns this reference object's referent. Because the referent of a 3 * phantom reference is always inaccessible, this method always returns 4 * <code>null</code>. 5 * 6 * @return <code>null</code> 7 */ 8 public T get() { 9 return null; 10 }
2)、虛引用的回收時機
虛引用與弱引用類似,都是在GC階段會被回收:
不同之處是弱引用在GC階段,一旦發現對象是弱引用,即被插入ReferenceQueue隊列中,而虛引用是在對象被銷毀後才會被放入ReferenceQueue隊列中。
3)、虛引用的必要性
乍看起來覺得虛引用存在的必要性非常弱,他的目的就是判斷GC是否已經開始了回收,這點功能其實上弱引用完全可以達到。但是恰恰是虛引用的不可達性,有時是必要的。
如下面三種場景下:
(1)在處理數據時,我們希望有些保密數據的引用是完全切斷的,不可達的。但是我們又希望可以知道這些數據是否已經被回收掉,那麼這時可以考慮虛引用。
(2)在引用變量變量被賦予新值後,我們希望無論通過何種情況,舊有引用指向的變量的都不被重新建立強引用(可能是代碼誤操作,或者是攻擊行為),這塊內存的數據在下次GC期間會被永久的刪除掉,這是也可以考慮虛引用。
(3)根據引用對象被插入到引用隊列的時機,我們希望知道對象在完全被銷毀後的時間點。
針對於第三個用途,很多人會疑惑,知道這個時間點,我們能有什麼用呢?(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )這裡先說一個題外話,搞過Java的人,基本都應該知道對象有一個和C++類似的回收方法:
protected void java.lang.Object.finalize()
這個方法需要各個需要調用的開發人員自己去復寫。為的就是在對象析構的時刻做很多事情。但是對象從回收到調用析構方法被調用是一個非常復雜的過程(這個我以後會講),所以在很多書中都介紹,不要去復寫析構方法,如注明的《Effective Java》。但是很多時候我們萬不得已,必須要在當前類被回收的時候做出一些行為。這時候就可以通過虛引用的形式,判斷當前對象是否已經被加入到引用隊列中,如果已經添加,那麼做出相應的行為即可。
這樣基本上就把四種引用的含義和使用多介紹完了。
最後的最後,很多人會認為引用的優先級如下:強引用>軟引用>弱引用>虛引用。
這裡個人覺得沒必要扯到優先級上,四種引用各有優劣,唯一的區別就是引用對象與內存之間的關系強度的大小。強度大的,可以讓JVM不回或晚回收。強度弱的,即使對象還沒有被回收,就無法通過引用獲取到內存信息。