1. 程式人生 > >Java強引用、軟引用、弱引用及虛引用深入探討

Java強引用、軟引用、弱引用及虛引用深入探討

強引用、軟引用、弱引用和虛引用深入探討

為了更靈活的控制物件的生命週期,在JDK1.2之後,引用被劃分為強引用、軟引用、弱引用、虛引用四種類型,每種型別有不同的生命週期,它們不同的地方就在於垃圾回收器對待它們會使用不同的處理方式。

引用型別在日常開發中並不常關注,也很少注意到,因此很多人忽略了它們的存在,而事實上,引用型別在Java體系中扮演著十分重要的角色,要想對Java體系有一個更深層次的理解,瞭解和掌握這些引用的用法是十分必要的。

在正式開始前,我們先來上兩道開胃菜。

為什麼需要回收

每一個Java程式中的物件都會佔用一定的計算機資源,最常見的,如:每個物件都會在堆空間上申請一定的記憶體空間。但是除了記憶體之外,物件還會佔用其它資源,如檔案控制代碼,埠,socket等等。當你建立一個物件的時候,必須保證它在銷燬的時候會釋放它佔用的資源。否則程式將會在OOM中結束它的使命。

在Java中不需要程式設計師來管理記憶體的分配和釋放,Java有自動進行記憶體管理的神器——垃圾回收器,垃圾回收器會自動回收那些不再使用的物件。

在Java中,不必像C或者C++那樣顯式去釋放記憶體,不需要了解其中回收的細節,也不需要擔心會將同一個物件釋放兩次而導致記憶體損壞。所有這些,垃圾回收器都自動幫你處理好了。你只需要保證那些不再被使用的物件的所有引用都已經被釋放掉了,否則,你的程式就會像在C++中那樣結束在記憶體洩漏中。

雖然垃圾回收器確實讓Java中的記憶體管理比C、C++中的記憶體管理容易許多,但是你不能對於記憶體完全不關心。如果你不清楚JVM到底會在什麼條件下才會對物件進行回收,那麼就有可能會不小心在程式碼中留下記憶體洩漏的bug。

因此,關注物件的回收時機,理解JVM中垃圾收集的機制,可以提高對於這個問題的敏感度,也能在發生記憶體洩漏問題時更快的定位問題所在。想了解更多關於垃圾回收相關的細節,可以參考這篇文章

為什麼需要引用型別

引用型別是與JVM密切合作的型別,有些引用型別甚至允許在其引用物件在程式中仍需要的時候被JVM釋放。

那麼,為什麼需要這些引用型別呢?

在Java中,垃圾回收器執行緒一直在默默的努力工作著,但你卻無法在程式碼中對其進行控制。無法要求垃圾回收器在精確的時間點對某些物件進行回收。

有了這些引用型別之後,可以一定程度上增加對垃圾回收的粒度把控,可以讓垃圾回收器在更合適的時機回收掉那些可以被回收掉的物件,而並不僅僅是隻回收不再使用的物件。

這些引用型別各有特點,各有各的適用場景,清楚的瞭解和掌握它們的用法可以幫助你寫出更加健壯的程式碼。

說明

在JDK 1.2以前的版本中,如果一個物件沒有被任何變數引用,那麼程式就無法再使用這個物件。也就是說,只有當物件處於可達(reachable)狀態時,程式才能使用它。只有在物件沒有任何其他物件引用它時,垃圾回收器才會對它進行收集。物件只有被引用和沒有被引用兩種狀態。這種方式無法描述一些“食之無味,棄之可惜”的物件。

而很多時候,我們希望存在這樣一些物件:當記憶體空間足夠時,可以將它們儲存在記憶體中,不進行回收;當記憶體空間變得緊張時,允許JVM回收這些物件。大部分快取都符合這樣的場景。

從JDK 1.2版本開始,Java對引用的概念進行了擴充,物件的引用分成了4種級別,從而使程式開發者能更加靈活地控制物件的生命週期,更好的控制建立的物件何時被釋放和回收。

這4種級別由高到低依次為:強引用軟引用弱引用虛引用

實力翻車

歡迎來到大型翻車現場,接下來將實力演示一波因為強引用過多導致的翻車例子。

如果你需要在整個程式執行期間儲存一些物件(因為它們的初始化很耗費時間和資源),你可能會使用靜態集合物件來儲存並且在程式碼中隨處使用它們。

public static Map<K, V> storedObjs = new HashMap<>();

但是這樣,就會阻止垃圾回收器對集合中的物件進行回收和銷燬。從而可能導致OOM的發生。例如:

public class OOMTest {
    public static List<Integer> cachedObjs = new ArrayList<>();
 
    public static void main(String[] args) {
        for (int i = 0; i < 100_000_000; i++) {
            cachedObjs.add(i);
        }
    }
}

輸出如下:

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

這樣就符合預期的翻車了。但你也許會說,誰會這麼無聊,建立這麼多變數。

嗯,確實是的,但是別忘了,一個程式可能會執行很長時間,幾個月,甚至幾年(如果你的程式碼和公司足夠健壯的話),如果期間不斷的建立變數而不清理的話(像上面那樣把HashMap當快取使用),是有可能會導致這種情況發生的。

內容編排

接下來的文章將從以下幾方面對這四種引用進行介紹:

注意 本系列文章都是以JDK1.8 版本的程式碼進行分析,不同版本中程式碼會略有差異。

如果只是想要對這些引用進行簡單瞭解,那麼看完簡要介紹部分即可,如果想要有更深入的研究,可以繼續查閱原始碼剖析部分。