總結Java中的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#


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM