美團一面:說一說Java中的四種引用型別?

码农Academy發表於2024-03-22

引言

在JDK1.2之前Java並沒有提供軟引用、弱引用和虛引用這些高階的引用型別。而是提供了一種基本的引用型別,稱為Reference。並且當時Java中的物件只有兩種狀態:被引用和未被引用。當一個物件被引用時,它將一直存在於記憶體中,直到它不再被任何引用指向時,才會被垃圾回收器回收。而被引用也就是強引用。

而在JDK1.2之後對引用的概念進行了擴充,分為了強引用(StrongReference)、軟引用(SoftReference)、弱引用(WeakReference)和虛引用(PhantomReference),這4種引用的強度依次減弱。他們的關係如下如:
image.png

強引用

強引用是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];  
}

此時我們執行方法後,發現報錯:

image.png
對於強引用,即使記憶體不夠使用,直接報錯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);  
    });  
}

然後我們執行程式碼之後:

image.png
對於列印結果中,只有最後一個物件保留了下來,其他的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,即都被回收了。

image.png

對於弱引用,就像我們正在旅行,使用一張一次性地圖。我們只在需要導航時使用地圖,一旦旅行結束,我們就不再需要地圖了。這時我們可以選擇扔掉地圖,類似於弱引用,在垃圾回收器執行時,無論記憶體是否充足,物件都可能被回收。

虛引用

在 JDK1.2之後,用java.lang.ref.PhantomReference類來表示虛引用。虛引用是最弱的引用型別,它幾乎對物件沒有任何影響,不能透過虛引用獲取物件,也不能透過它來阻止物件被垃圾回收。從原始碼中可以看出它只有一個建構函式和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是說將永遠無法透過虛引用來獲取物件,虛引用必須要和 ReferenceQueue 引用佇列一起使用。

image.png

虛引用可以用於在物件被回收時進行後續操作,如物件資源釋放或日誌記錄,常用於跟蹤物件被垃圾回收的狀態,執行一些清理工作。

而對於弱引用,就像我們去商店,商店入口處的門閂並不直接影響你進入房屋,但它會在有人進入或離開時發出聲音,提醒你有人進店(歡迎光臨)或者離開(歡迎再來)。類似地,虛引用並不直接影響物件的生命週期,但它可以在物件被回收時發出通知,讓你有機會進行一些後續操作,比如資源釋放或者記錄日誌。

引用佇列

引用佇列(ReferenceQueue)是Java中的一個特殊佇列,用於配合軟引用、弱引用和虛引用,實現更靈活的物件引用和回收管理。

引用佇列的主要作用是跟蹤物件的垃圾回收過程。當一個軟引用、弱引用或虛引用指向的物件被垃圾回收器回收時,如果它們與一個引用佇列關聯,那麼這些引用就會被自動加入到引用佇列中。透過監視引用佇列中的物件,我們可以瞭解到物件的回收狀態,從而執行一些額外的操作,比如資源釋放或日誌記錄等。

總結

Java中的四種引用型別各具特點,可根據程式需求選擇合適的引用型別。強引用保證物件不被意外回收,軟引用和弱引用用於實現快取或解決記憶體敏感問題,而虛引用則用於物件回收後的通知和清理操作。合理使用引用型別可以更好地管理記憶體和避免記憶體洩漏問題。

本文已收錄於我的個人部落格:碼農Academy的部落格,專注分享Java技術乾貨,包括Java基礎、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中介軟體、架構設計、面試題、程式設計師攻略等

相關文章