總結Java中的reference類型與四種引用類型

来源:https://www.cnblogs.com/allentechblog/archive/2020/05/16/12902523.html
-Advertisement-
Play Games

本文通過分析源碼和實驗測試總結了Java中的reference類型、Reference類以及四種引用類型的基礎知識。 僅做學習記錄目的,有誤的歡迎指出! ...


總結Java中的reference類型與四種引用類型

本文通過分析源碼和實驗測試總結了Java中的reference類型、Reference類以及四種引用類型的基礎知識。
僅做學習記錄目的,有誤的歡迎指出!

一、什麼是reference類型

Java數據類型分為兩大類:

基本類型 (primitive type)

8種基本類型 byte, short, int, long, float, double, char , boolean

引用類型(reference)

《Java虛擬機規範》中寫道:

Java虛擬機中有三種引用類型:類類型(class type)、數組類型(array type)和介面類型(interface type)。這些引用類型的值分別指向動態創建的類實例、數組示例和實現了某個介面的類示例或數組示例。

可見,引用類型的值其實就是實例在堆記憶體上的地址,可以把引用近似理解為指針。

引用中的null值:當一個引用不指向任何對象的時候,它的值就用null來表示。引用的預設值就是null。

JVM應能通過引用實現兩點:

  1. 從該引用直接或間接地查找到對象在堆中的數據存放的起止地址索引。
  2. 從該引用中直接或間接地查到對象所屬類在方法區中存儲的類型信息。

這是很容易理解的,比如下麵的代碼:

User ref = new User();
ref.getUsername();	// 通過引用獲取該類的實例的數據
ref.getClass();		// 通過引用獲取該類的類型信息 (Class對象)

實際上,在HotSpot的實現中,reference的值並不直接指向實例,而是指向一個句柄,由句柄再指向實際的實例。這樣的好處時,在對象實例數據在記憶體中的位置被移動時(比如GC時),不需要修改棧上所有相關的reference的值,只需要修改句柄的值(只需要修改一次),代價是多一次的定址。

二、什麼是Reference類

Reference類是Java.lang.ref包里的一個抽象類,源碼中對其的描述是:

Abstract base class for reference objects. This class defines the operations common to all reference objects.

我把這個Reference類理解為 (也許不准確):描述reference類型的類,這個類定義了reference類型的行為,提供了reference類型的基本功能。就像Integer類之於int類型。

Reference對象可以“註冊”相關的引用對象,並通過內部的reference隊列提供外部程式監控對象被GC的能力。

部分源碼:

/**
* 被註冊的引用對象
*/
private T referent;         /* Treated specially by GC */
/**
* 當一個Reference對象綁定的對象被GC回收時,JVM會將該引用對象被綁定到的reference對象(this)推入此隊列。
* 其他程式可以通過輪詢此隊列,來獲得該註冊對象被GC的的“通知”,並完成一些工作
* 如WeakHashMap可以"知道"被GC的Entry並將其從Map中移除
* 實際只是邏輯上的一個標誌,標誌該對象是否加入到了隊列。
* 隊列里的Reference對象是通過next屬性組成鏈式迴圈隊列
*/
volatile ReferenceQueue<? super T> queue;
volatile Reference next;

Reference(T referent) {
    this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}

/**
* 返回註冊的引用對象,若對象已被GC,返回null
*/
public T get() {
    return this.referent;
}
/**
* 清除註冊到該對象的引用對象。但是並不會加入referenceQueue
*/
public void clear() {
    this.referent = null;
}
/**
*將"註冊"的對象,加入referenceQueue。
*/
public boolean enqueue() {
    this.referent = null;
    return this.queue.enqueue(this);
}

三、四種引用類型

JVM把引用類型分為四種類型:強引用、軟引用、弱引用、虛引用,引用的類型可以描述它所指向的實例的可達性,進而供垃圾回收器根據不同類型做出不同的處理的能力,同時也提供了編程者跟蹤對象生命周期的功能。

描述不同的引用類型,由Reference類的子類來實現:

  • FinalReference(強引用)
  • SoftReference (軟引用)
  • WeakReference (弱引用)
  • PhantomReference (虛引用)

1、 FinalReference 強引用

強引用是指創建一個對象並它賦值給一個引用,引用是存在JVM中的棧(還有方法區)中的。具有強引用的對象,垃圾回收器絕對不會去回收它,直到記憶體不足以分配時,拋出OOM。

大多數情況,我們new一個對象,並把它賦值給一個變數,這個變數就是強引用。

class TestA {
    // 方法區中的類靜態屬性引用的對象
    private static Object finalRet2 = new Object();
    // 方法區中的常量引用的對象
    private static final Object finalRet3 = new Object();
    
    void methodA {
        // 棧上的局部變數引用的對象
		Object finalRet1 = new Object();
    }
    
    native void methodB {
        // JNI中引用的對象
		// ......
    }
}

以上指向的實例對象,是可達的。

FinalReference 類只用於實現Finalize功能,非public類,用戶是不可用的

2、SoftReference 軟引用

軟引用描述一些還有用但非必需的對象

具有軟引用關聯的對象,記憶體空間足夠時,垃圾回收器不會回收它。當記憶體不足時(接近OOM),垃圾回收器才會去決定是否回收它。

軟引用一般用來實現簡單的記憶體緩存。

我們通過以下測試代碼來驗證它的特性:

public class ReferenceTest {

    class User {
        // 模擬記憶體占用3M,以更好觀察gc前後的記憶體變化
        private byte[] memory = new byte[3*1024*1024];
    }
    /**
     * 測試弱引用在記憶體足夠時不會被GC,在記憶體不足時才會被GC的特性
     * JVM參數 -Xms20m -Xmx20m -Xlog:gc  將記憶體大小限制在20M,並列印出GC日誌
     */
    public void testSoftReference(){

        // 當僅使用強引用,脫離GC Root後將會被回收 (可以通過查看gc日誌來確認該對象確實被回收)
        // 這是對照組
        User retA = new User();
        retA = null;
        System.gc();
        System.out.println("對照組GC後:" + retA);


        User retB = new User();
        // 創建弱引用類,將該引用綁定到弱引用對象上
        SoftReference<User> sortRet = new SoftReference<>(retB);
        retB = null;
        // 此時並不會被GC
        System.gc();
        retB = sortRet.get();
        System.out.println("GC後通過軟引用重新獲取了對象:" + retB);

        retB = null;

        // 模擬記憶體不足,即將發生OOM
        List<User> manyUsers = new ArrayList<>();
        for(int i = 1; i < 100000; i++){
            System.out.println("將要創建第" + i + "個對象");
            manyUsers.add(new User());
            System.out.println("創建第" + i + "個對象後, 軟引用對象:" + sortRet.get());
        }
    }


    public static void main(String[] args) {
        ReferenceTest referenceTest = new ReferenceTest();
        referenceTest.testSoftReference();
    }
}

執行結果如下:

3、WeakReference 弱引用

弱引用描述非必需對象,但它的強度比軟引用更弱一些。

WeakReference對其引用的對象並無保護作用,當垃圾回收器進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的對象。弱引用一般用於實現canonicalizing mappings (正規化映射),典型的應用是WeakHashMap。

我們通過以下代碼來驗證它的特性:

/**
* 測試弱引用無論記憶體是否足夠都會被GC的特性
*/
public void testWeakReference(){
    User user = new User();
    WeakReference<User> ret = new WeakReference<>(user);
    System.out.println("GC前: " + ret.get());
    user = null;
    System.gc();
    System.out.println("GC後: " + ret.get());
}

執行結果:

[0.014s][info][gc] Using G1
[0.033s][info][gc] Periodic GC disabled
GC前: memory.ReferenceTest$User@b4c966a
[0.098s][info][gc] GC(0) Pause Full (System.gc()) 6M->0M(20M) 2.815ms
GC後: null

4、PhantomReference 虛引用

虛引用也被稱為幽靈引用或幻引用,它是最弱的一種引用關係。

虛引用並不會影響對象的GC,而且並不可以通過PhantomReference對象取得一個引用的對象。

虛引用唯一的作用則是利用其必須和ReferenceQueue關聯使用的特性,當其綁定的對象被GC回收後會被推入ReferenceQueue,外部程式可以通過對此隊列輪詢來獲得一個通知,以完成一些目標對象被GC後的清理工作。

PhantomReference 的構造方法,與SoftReference和WeakReference不同,他的構造必須傳入一個ReferenceQueue

public PhantomReference(T referent, ReferenceQueue<? super T> q) {    super(referent, q);}

四、應用

1、軟引用實現記憶體緩存

上文提到,軟引用關聯的對象在記憶體足夠時不會被GC清理,在記憶體不足時才會被GC清,結合我們可以通過ReferenceQueue獲取一個被GC的對象的Reference引用對象的能力,我們可以實現一個簡單的記憶體緩存,該緩存在JVM記憶體不足時能夠自動清理,在記憶體充足時可以自動裝入。

實現代碼:

/**
 * @ClassName SoftRefCache
 * @Description 軟引用實現的記憶體緩存(僅做學習目的,實際項目當然是用造好的輪子,memcached、redis等)
 */
public class SoftRefCache<K, V> {

    // 實際裝載緩存的數據結構,採用Hashtable可以保證線程安全
    private final Hashtable<K, ValueRef> cache;
    // 此隊列用來接收被GC的引用對象,來完成清理工作
    private final ReferenceQueue<V> queue;
    // 當被緩存對象不存在緩存中時,調用該介面來查詢此對象,以裝入緩存
    private final QueryForCache<K,V> queryForCache;

    public SoftRefCache(QueryForCache<K,V> queryForCache) {
        this.cache = new Hashtable<>();
        this.queue = new ReferenceQueue<>();
        this.queryForCache = queryForCache;
    }

    /**
     * 對value的包裝,使用軟引用來關聯value對象,使其具有軟引用的對象特性,並保存該value對象的key,以便於完成清理工作
     */
    private class ValueRef extends SoftReference<V> {

        private final K key;

        public ValueRef(K key, V referent, ReferenceQueue<? super V> q) {
            super(referent, q);
            this.key = key;
        }

        public K getKey() {
            return key;
        }
    }

    /**
     * 由Key獲取一個對象,若已被緩存,則直接返回,若未被緩存,則將其緩存
     * @param key 要獲取的對象的eky
     * @return 要獲取的對象
     */
    public V get(K key) {
        V val = null;
        if (cache.containsKey(key)) {
            ValueRef valueRef = cache.get(key);
            val = valueRef != null ? valueRef.get() : null;
        }
        // cache中沒有該key對應的對象實例
        if (val == null) {
            // 到資料庫或硬碟查詢該對象,並加入到cache中
            val = this.queryForCache.query(key);
            addToCache(key, val);
        }
        return val;
    }

    /**
     * 獲取緩存內key--value對的數量
     */
    public int size(){
        this.clearCache();
        return cache.size();
    }
    
    /**
     * 清除緩存
     */
    public void clearAllCache(){
        clearCache();
        cache.clear();
        // 可以根據實際情況決定是否要GC
        System.gc();

    }

    /**
     * 將對象加入緩存
     */
    private void addToCache(K key, V val){
        // 清除垃圾引用
        clearCache();
        // 加入到緩存
        ValueRef valueRef = new ValueRef(key, val, queue);
        this.cache.put(key, valueRef);
    }

    /**
     * 清除緩存中已被GC的Value對象。
     * 具體是通過對ReferenceQueue輪詢來實現的
     */
    private void clearCache(){
        ValueRef valueRef = null;
        while((valueRef = (ValueRef) queue.poll()) != null){
            cache.remove(valueRef.getKey());
        }
    }

}

/**
 * 該介面定義了一個需要緩存的對象不在緩存時,應該通過怎樣的方式獲取
 * @param <K> key的類型
 * @param <V> value的類型
 */
@FunctionalInterface
interface QueryForCache<K,V> {
    V query(K key);

測試代碼:

/**
 * @ClassName SortRefCacheTest
 * @Description 測試自己實現的軟引用緩存,JVM參數:-Xms20m -Xmx20m -Xlog:gc
 */
public class SortRefCacheTest {

    public static void main(String[] args) {
        // 這個介面實際應該實現為到資料庫或硬碟查詢實際的數據,這裡就簡單模擬,直接new
        QueryForCache<Integer, MyImage> queryForCache = key -> new MyImage(key, new byte[2*1024*1024]);
        // 創建緩存
        SoftRefCache<Integer, MyImage> softRefCache = new SoftRefCache<>(queryForCache);
        // 此處模擬不斷對緩存進行裝入,觀察記憶體和gc情況
        for(int i=1; i < 100; i++){
            MyImage value = softRefCache.get(i);
            System.out.println("從緩存中獲取到第" + value.getId() + "個MyImage");
        }
    }

}
class MyImage {
    private Integer id;
    private byte[] data; // 模擬較大的記憶體占用,以更好觀察gc前後的記憶體變化

    public MyImage(Integer id, byte[] data) {
        this.id = id;
        this.data = data;
    }

    public Integer getId() {
        return id;
    }
}

執行結果(部分):

執行到最後,並沒有拋出OOM

如果使用普通的HashMap等容器,結果就是OOM,這裡就不驗證了

參考文獻

  1. 《深入理解Java虛擬機》 第二版 周志明著

  2. JAVA中reference類型簡述 https://www.iteye.com/blog/shift-alt-ctrl-1839163

  3. JAVA四種引用方式 https://blog.csdn.net/u014086926/article/details/52106589#


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 實踐內容 從 MariaDB 一張表內讀 10 萬條記錄,經處理後寫到 MongoDB 。 具體實現 1、新建 Spring Boot 應用,依賴如下: 2、創建一張表,並生成 10 萬條數據 3、創建 Person 類 4、創建一個中間處理器 5、創建 ,用戶資料庫映射 6、創建任務完成的監聽 7 ...
  • (一)棧 1、棧是一種後進先出,先進後出的數據結構。 2、棧是一種操作受限的線性表,只允許在一端插入和刪除數據。 3、棧主要包含2個操作,入棧和出棧 4、棧可以用數組實現,也可以用鏈表實現。用數組實現的棧叫做順序棧,用鏈表實現的棧叫做鏈式棧。 例如: 現在有一個空瓶子。 1、我們依次放入多個蘋果 2 ...
  • 我的LeetCode:https://leetcode cn.com/u/ituring/ 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 面試題58 I. 翻轉單詞順序 與以下題目相同 前往:Leet ...
  • 我的LeetCode:https://leetcode cn.com/u/ituring/ 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 151. 翻轉字元串里的單詞 題目 給定一個字元串,逐個翻轉字 ...
  • 編程:在屏幕中間分別顯示綠色,綠色紅底,白底藍色的字元串 'welcome to masm!'. assume cs:code,ds:data,ss:stack data segment db 'welcome to masm!' db 00000010b,00100100b,01110001b # ...
  • 對於類和封裝的學習,學習了private這個函數,但是到練習的時候,出現了問題...... 問題描述: 類裡面的年齡(age)使用了private函數,在srtAge裡面添加了一個判斷,(用來判斷數字,當age>=18的時候右邊運行還是會顯示else里的語句,只有age>=0的時候,才會運行正常。想 ...
  • 聚合一般作用在query範圍內。不帶query的aggregation請求實際上是在match_all{}查詢範圍內進行統計的: GET /cartxns/_search { "aggs": { "all_colors": { "terms": {"field" : "color.keyword"} ...
  • 我的LeetCode:https://leetcode cn.com/u/ituring/ 我的LeetCode刷題源碼[GitHub]:https://github.com/izhoujie/Algorithmcii LeetCode 25. K 個一組翻轉鏈表 題目 給你一個鏈表,每 k 個節點 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...