強引用、軟引用、弱引用和虛引用深入探討
為了更靈活的控制對象的生命周期,在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
版本的代碼進行分析,不同版本中代碼會略有差異。
如果只是想要對這些引用進行簡單了解,那么看完簡要介紹部分即可,如果想要有更深入的研究,可以繼續查閱源碼剖析部分。