引言
在JDK1.2之前Java並沒有提供軟引用、弱引用和虛引用這些高階的引用型別。而是提供了一種基本的引用型別,稱為Reference
。並且當時Java中的物件只有兩種狀態:被引用和未被引用。當一個物件被引用時,它將一直存在於記憶體中,直到它不再被任何引用指向時,才會被垃圾回收器回收。而被引用也就是強引用。
而在JDK1.2之後對引用的概念進行了擴充,分為了強引用(StrongReference
)、軟引用(SoftReference
)、弱引用(WeakReference
)和虛引用(PhantomReference
),這4種引用的強度依次減弱。他們的關係如下如:
強引用
強引用是Java中最常見的引用型別。當你建立一個物件並將其賦值給一個變數時,這個變數會持有該物件的強引用。
Order order = new Order(); // 只要order還指向Order物件,那麼Order物件就不會被回收
order = null; // 強引用都被設定為 null 時,不可達,則Order物件被回收
只要存在強引用指向物件,垃圾回收器將永遠不會回收該物件,即使記憶體不足也不會回收。這可能導致記憶體溢位,因為即使記憶體不足,JVM也不會回收強引用物件。當強引用都被設定為null時,物件變成不可達狀態,垃圾回收器會在適當的時候將其回收。
比如以下示例,我們建立一個2M的陣列,但是我們設定JVM引數:-Xms2M -Xmx3M
,將JVM的初始記憶體設為2M,最大可用記憶體為3M。
public static void main(String[] args) {
//定義一個2M的陣列
byte[] objects = new byte[1024 * 1024 * 2];
}
此時我們執行方法後,發現報錯:
對於強引用,即使記憶體不夠使用,直接報錯OOM,強引用也不會被回收。
對於強引用,就好比生活中,當我們擁有家裡的鑰匙時,我們可以隨時進入你的家,即使我們不需要進入,也能確保我們可以進入。鑰匙是我們進入家的強引用。只有當我們不再擁有鑰匙時,我們才無法進入家,類似於當沒有強引用指向一個物件時,該物件才能被垃圾回收。
軟引用
在JDK1.2之後,用java.lang.ref.SoftReference
類來表示軟引用。軟引用允許物件在記憶體不足時被垃圾回收器回收。如果一個物件只有軟引用指向它,當系統記憶體不足時,垃圾回收器會嘗試回收這些物件來釋放記憶體,如果回收了軟引用物件之後仍然沒有足夠的記憶體,才會丟擲記憶體溢位異常。軟引用適用於需要快取大量物件,但又希望在記憶體不足時釋放部分物件以避免記憶體溢位的情況,用於實現快取時,當記憶體緊張時,可以釋放部分快取物件以保證系統的穩定性。
以下示例我們設定JVM引數為:-Xms3M -Xmx5M
,然後連續建立了10個大小為1M的位元組陣列,並賦值給了軟引用,然後迴圈遍歷將這些物件列印出來。
private static final List<Object> list = Lists.newArrayList();
public static void main(String[] args) {
IntStream.range(0, 10).forEach(i -> {
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<>(buff);
list.add(sr);
});
System.gc(); // 主動通知垃圾回收
list.forEach(l -> {
Object obj = ((SoftReference<?>) l).get();
System.out.println("Object: " + obj);
});
}
然後我們執行程式碼之後:
對於列印結果中,只有最後一個物件保留了下來,其他的obj全都被置空回收了。即說明了在記憶體不足的情況下,軟引用將會被自動回收。
對於弱引用,就像我們醫藥箱裡的備用藥,當我們需要藥品時,我們會先看看醫藥箱裡是否有備用藥。如果醫藥箱裡有足夠的藥品(記憶體足夠),我們就可以使用備用藥;但如果醫藥箱裡的備用藥不夠了(記憶體不足),我們可能會去藥店購買。在記憶體不足時,垃圾回收器可能會回收軟引用物件,類似於我們在醫藥箱裡的備用藥被用完時去藥店購買。
弱引用
JDK1.2之後,用java.lang.ref.WeakReference
來表示弱引用。弱引用與軟引用類似,但強度更弱。即使記憶體足夠,只要沒有強引用指向一個物件,垃圾回收器就可以隨時回收該物件。弱引用適用於需要臨時引用物件的場景,如臨時快取或臨時儲存物件。也可以用於解決物件之間的迴圈引用問題,避免記憶體洩漏。
對於上述示例中,我們將陣列賦值給弱引用
private static final List<Object> list = Lists.newArrayList();
public static void main(String[] args) {
IntStream.range(0, 10).forEach(i -> {
byte[] buff = new byte[1024 * 1024];
WeakReference<byte[]> sr = new WeakReference<>(buff);
list.add(sr);
});
System.gc(); // 主動通知垃圾回收
list.forEach(l -> {
Object obj = ((WeakReference<?>) l).get();
System.out.println("Object: " + obj);
});
}
執行結果發現所有的物件都是null,即都被回收了。
對於弱引用,就像我們正在旅行,使用一張一次性地圖。我們只在需要導航時使用地圖,一旦旅行結束,我們就不再需要地圖了。這時我們可以選擇扔掉地圖,類似於弱引用,在垃圾回收器執行時,無論記憶體是否充足,物件都可能被回收。
虛引用
在 JDK1.2之後,用java.lang.ref.PhantomReference
類來表示虛引用。虛引用是最弱的引用型別,它幾乎對物件沒有任何影響,不能透過虛引用獲取物件,也不能透過它來阻止物件被垃圾回收。從原始碼中可以看出它只有一個建構函式和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是說將永遠無法透過虛引用來獲取物件,虛引用必須要和 ReferenceQueue 引用佇列一起使用。
虛引用可以用於在物件被回收時進行後續操作,如物件資源釋放或日誌記錄,常用於跟蹤物件被垃圾回收的狀態,執行一些清理工作。
而對於弱引用,就像我們去商店,商店入口處的門閂並不直接影響你進入房屋,但它會在有人進入或離開時發出聲音,提醒你有人進店(歡迎光臨)或者離開(歡迎再來)。類似地,虛引用並不直接影響物件的生命週期,但它可以在物件被回收時發出通知,讓你有機會進行一些後續操作,比如資源釋放或者記錄日誌。
引用佇列
引用佇列(ReferenceQueue
)是Java中的一個特殊佇列,用於配合軟引用、弱引用和虛引用,實現更靈活的物件引用和回收管理。
引用佇列的主要作用是跟蹤物件的垃圾回收過程。當一個軟引用、弱引用或虛引用指向的物件被垃圾回收器回收時,如果它們與一個引用佇列關聯,那麼這些引用就會被自動加入到引用佇列中。透過監視引用佇列中的物件,我們可以瞭解到物件的回收狀態,從而執行一些額外的操作,比如資源釋放或日誌記錄等。
總結
Java中的四種引用型別各具特點,可根據程式需求選擇合適的引用型別。強引用保證物件不被意外回收,軟引用和弱引用用於實現快取或解決記憶體敏感問題,而虛引用則用於物件回收後的通知和清理操作。合理使用引用型別可以更好地管理記憶體和避免記憶體洩漏問題。
本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等