天天看點

[轉載] Java的四種引用關系

Java中提供了4個級别的引用: 強引用、軟引用、弱引用和虛引用, 這四個引用定義在包`java.lang.ref`下. 本篇博文通過代碼詳細示範不同引用類型的作用, 如有不當之處, 歡迎評論區交流.

目錄

  • 1 強引用 (Final Reference)
  • 2 軟引用 (Soft Reference)
    • 2.1 案例1: 軟引用的垃圾回收
    • 2.2 案例2: 軟引用緩存的使用
    • 2.3 軟引用的應用場景
  • 3 弱引用 (Weak Reference)
  • 4 虛引用 (Phantom Reference)
  • 參考資料
  • 版權聲明

版權聲明: 本文為轉載文章, 轉載時有适量修改. 再次轉載時請附上原文出處連結和本聲明.

原文連結: https://blog.csdn.net/u013256816/article/details/50907595

Java中提供了4個級别的引用: 強引用、軟引用、弱引用和虛引用, 這四個引用定義在包

java.lang.ref

下:

[轉載] Java的四種引用關系

強引用就是指在程式代碼中普遍存在的、類似于

Object obj = new Object()

的引用, 隻要強引用還存在, 垃圾收集器就不會去回收這些被引用的對象.

強引用有以下三個特點:

  1. 強引用可以直接通路目标對象;
  2. 強引用鎖指向的對象在任何時候都不會被系統回收 ——JVM甯願抛出OOM異常也不回收強引用所指向的對象;
  3. 強引用可能導緻記憶體洩露, 比如List中添加了new出來的對象, List失去被回收之後, 其内部的對象不能被通路、卻又不會被回收的現象.

FinalReference類的全部定義如下:

package java.lang.ref;
/**
 * Final references, used to implement finalization
 */
class FinalReference<T> extends Reference<T> {
    public FinalReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}
           

FinalReference隻有一個構造函數: 根據給定對象的引用和引用隊列構造一個強引用.

軟引用用來描述一些還有用但并非必須的對象: 被軟引用關聯着的對象, 如果記憶體充足, 則垃圾回收器不會回收該對象;

如果記憶體不夠用, 就會回收這些對象.

在系統将要發生

OutOfMemoryError

之前, JVM會把被軟引用關聯着的對象列進回收範圍, 并進行第二次回收. 如果這次回收之後記憶體仍然不夠用, 系統才會抛出

OutOfMemoryError

.

從JDK 1.2開始提供了SoftReference類來實作軟引用: 與一個引用隊列 (ReferenceQueue) 聯合使用實作記憶體敏感的高速緩存 —— 如果軟引用所引用的對象被垃圾回收器回收, JVM就會把這個軟引用加入到與之關聯的引用隊列中.

package com.healchow.java.detail;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;

public class SoftRefTest {
    private static ReferenceQueue<MyObject> softQueue = new ReferenceQueue<>();

    private static class MyObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }
        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) softQueue.remove();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (obj != null) {
                System.out.println("Object for SoftReference is " + obj.get());
            }
        }
    }
    
    public static void main(String[] args) {
        MyObject obj = new MyObject();
        SoftReference<MyObject> softRef = new SoftReference<>(obj, softQueue);
        new Thread(new CheckRefQueue()).start();

        // 删除強引用, 否則obj不會被回收
        obj = null;
        System.gc();
        System.out.println("After GC: Soft Get = " + softRef.get());
        System.out.println("嘗試配置設定大塊記憶體...");
        byte[] b = new byte[5 * 1024 * 725];
        System.out.println("After new byte[]: Soft Get = " + softRef.get());
        System.gc();
    }
}
           

(1) 測試方法1:

在運作測試主方法時設定VM參數:

-Xmx5M

, 也就是指定該程式的Java Heap最大為5MB, 運作結果為:

After GC: Soft Get = I am MyObject
嘗試配置設定大塊記憶體...
After new byte[]: Soft Get = I am MyObject
MyObject's finalize called
Object for SoftReference is null
           

案例代碼解釋:

① 首先構造MyObject對象, 并将其指派給object變量, 構成強引用.

② 然後使用SoftReference構造這個MyObject對象的軟引用softRef, 并注冊到softQueue引用隊列 —— 當softRef被回收時, 會被加入softQueue隊列.

③ 設定

obj = null

, 删除這個強引用, 這時系統内對MyObject對象的引用隻剩下軟引用.

④ 顯示調用GC, 通過軟引用的get()方法擷取MyObject對象的引用, 發現對象并未被回收, 這說明GC在記憶體充足的情況下, 不會回收軟引用對象.

⑤ 接着請求一塊大的堆空間

5*1024*725

(要多次調整使得垃圾回收工作能順利進行、線程能順利退出), 這個操作會使系統堆記憶體使用緊張, 進而産生新一輪的GC. 在這次GC後,

softRef.get()

不再傳回MyObject對象, 而是傳回null —— 說明在系統記憶體緊張的情況下, 軟引用被回收. 軟引用被回收時, 會被加入注冊的引用隊列, 此時引用隊列中有了元素, 開辟的多線程中

softQueue.remove()

不再阻塞, 是以程式得以成功退出.

如果将上面案例中的數組再改大點, 比如

5*1024*1024

, 就會抛出OOM異常:

After GC: Soft Get = I am MyObject
嘗試配置設定大塊記憶體...
MyObject's finalize called
Exception in thread "main" Object for SoftReference is null
java.lang.OutOfMemoryError: Java heap space
	at com.healchow.java.detail.SoftRefTest.main(SoftRefTest.java:56)
           

(2) 測試方法2:

-Xmx5M -XX:PrintGCDetails

, 列印出GC的日志資訊(關于GC日志可以檢視《Java堆記憶體http://blog.csdn.net/u013256816/article/details/50764532》), 運作結果為:

[GC (Allocation Failure) [PSYoungGen: 1024K->480K(1536K)] 1024K->480K(5632K), 0.0013139 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (System.gc()) [PSYoungGen: 1383K->480K(1536K)] 1383K->544K(5632K), 0.0011186 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 480K->0K(1536K)] [ParOldGen: 64K->504K(4096K)] 544K->504K(5632K), [Metaspace: 3316K->3316K(1056768K)], 0.0044642 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
After GC: Soft Get = I am MyObject
嘗試配置設定大塊記憶體...
[GC (Allocation Failure) [PSYoungGen: 38K->64K(1536K)] 542K->568K(5632K), 0.0009263 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 64K->32K(1536K)] 568K->536K(5632K), 0.0011345 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 32K->0K(1536K)] [ParOldGen: 504K->504K(4096K)] 536K->504K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0038031 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 504K->504K(5632K), 0.0007999 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 504K->486K(4096K)] 504K->486K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0037241 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
MyObject's finalize called
Object for SoftReference is null
After new byte[]: Soft Get = null
[Full GC (System.gc()) [PSYoungGen: 59K->0K(1536K)] [ParOldGen: 4086K->4083K(4096K)] 4145K->4083K(5632K), [Metaspace: 3317K->3317K(1056768K)], 0.0036086 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 1536K, used 10K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 1% used [0x00000007bfe00000,0x00000007bfe02a68,0x00000007bff00000)
  from space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 4096K, used 4083K [0x00000007bfa00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 4096K, 99% used [0x00000007bfa00000,0x00000007bfdfcfc0,0x00000007bfe00000)
 Metaspace       used 3324K, capacity 4564K, committed 4864K, reserved 1056768K
  class space    used 366K, capacity 388K, committed 512K, reserved 1048576K
           

public class BitMapManager {
    private Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();

    // 儲存Bitmap的軟引用到HashMap
    public void saveBitmapToCache(String path) {
        // 強引用的Bitmap對象
        Bitmap bitmap = BitmapFactory.decodeFile(path);
        // 軟引用的Bitmap對象
        SoftReference<Bitmap> softBitmap = new SoftReference<Bitmap>(bitmap);
        // 添加該對象到Map中使其緩存
        imageCache.put(path, softBitmap);
        // 使用完後手動将位圖對象置null
        bitmap = null;
    }

    public Bitmap getBitmapByPath(String path) {
        // 從緩存中取軟引用的Bitmap對象
        SoftReference<Bitmap> softBitmap = imageCache.get(path);
        // 判斷是否存在軟引用
        if (softBitmap == null) {
            return null;
        }
        // 取出Bitmap對象,如果由于記憶體不足Bitmap被回收,将取得空
        Bitmap bitmap = softBitmap.get();
        return bitmap;
    }
}
           

軟引用主要用于記憶體敏感的高速緩存, 在Android系統中經常用到. 大多數 Android應用都會用到大量的圖檔. 由于讀取檔案需要硬體操作, 速度較慢, 是以考慮将圖檔緩存起來, 需要的時候直接從記憶體中讀取.

但由于圖檔占用記憶體空間比較大, 緩存很多圖檔就可能容易發生OutOfMemoryError, 這時我們可以考慮使用軟引用技術來避免這個問題發生.

SoftReference可以解決OOM的問題: 每一個對象通過軟引用進行執行個體化, 這個對象就以cache的形式儲存起來, 再次調用這個對象時, 就可以直接通過軟引用中的

get()

方法得到對象中的資源資料. 當記憶體将要發生OOM的時候, GC會迅速把所有的軟引用清除, 防止OOM發生.

弱引用用來描述非必須的對象, 它的強度比軟引用更弱, 被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前. 當垃圾收集器工作時, 無論目前記憶體是否足夠, 都會回收掉隻被弱引用關聯的對象. 一旦一個弱引用對象被垃圾回收器回收, 便會加入到一個注冊引用隊列中.

我們略微修改一下上一節案例1的代碼:

package com.healchow.java.detail;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class WeakRefTest {
    private static ReferenceQueue<MyObject> weakQueue = new ReferenceQueue<>();

    private static class MyObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }
        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>)weakQueue.remove();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(obj != null) {
                System.out.println("删除的弱引用為: " + obj);
                System.out.println("擷取弱引用的對象 obj.get() 為: " + obj.get());
            }
        }
    }

    public static void main(String[] args) {
        MyObject object = new MyObject();
        Reference<MyObject> weakRef = new WeakReference<>(object,weakQueue);
        System.out.println("建立的弱引用為: " + weakRef);
        new Thread(new CheckRefQueue()).start();

        object = null;
        System.out.println("Before GC: Weak Get = " + weakRef.get());
        System.gc();
        System.out.println("After GC: Weak Get = " + weakRef.get());
    }
}
           

(1) 示範方法 —— 不修改JVM參數:

運作結果為:

建立的弱引用為: java.lang.ref.WeakReference@6f94fa3e
Before GC: Weak Get = I am MyObject
After GC: Weak Get = null
删除的弱引用 obj 為: java.lang.ref.WeakReference@6f94fa3e
但是擷取弱引用的對象 obj.get() 為: null
MyObject's finalize called
           

可以看到:

在GC之前, 弱引用對象并未被垃圾回收器發現, 是以通過

weakRef.get()

可以擷取對應的對象引用.

但是隻要進行垃圾回收, 弱引用就會被發現, 并立即被回收, 并加入注冊引用隊列中. 此時再試圖通過

weakRef.get()

擷取對象的引用就會失敗.

(2) 弱引用的使用場景:

弱引用的使用場景可參考

java.util.WeakHashMap

軟引用、弱引用都非常适合來儲存那些可有可無的緩存資料. 系統記憶體不足時, 這些緩存資料會被回收, 就不會導緻記憶體溢出. 而當記憶體資源充足時, 這些緩存資料又可以存在相當長的時間, 進而提高系統的響應速度用.

虛引用也稱為幽靈引用或者幻影引用, 它是最弱的一種引用關系. 一個持有虛引用的對象, 和沒有引用幾乎是一樣的 —— 随時都有可能被垃圾回收器回收.

當試圖通過虛引用的

get()

方法取得強引用時, 總是會失敗.

并且虛引用必須和引用隊列一起使用, 它的作用在于跟蹤垃圾回收過程.

虛引用中

get()

方法永遠傳回null, 其實作如下:

public T get() {
    return null;
}
           

我們再來修改第2節案例1的代碼:

package com.healchow.java.detail;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.TimeUnit;

public class PhantomRefTest {
    private static ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();

    private static class MyObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }
        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> obj = null;
        @Override
        public void run() {
            try {
                obj = (Reference<MyObject>) phantomQueue.remove();
                System.out.println("删除的虛引用 obj 為: " + obj);
                System.out.println("但是擷取虛引用的對象 obj.get() 為: " + obj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        Reference<MyObject> phantomRef = new PhantomReference<>(object, phantomQueue);
        System.out.println("建立的虛引用為:" + phantomRef);
        new Thread(new CheckRefQueue()).start();

        object = null;
        TimeUnit.SECONDS.sleep(1);
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "次gc");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}
           
建立的虛引用為:java.lang.ref.PhantomReference@6f94fa3e
第1次gc
MyObject's finalize called
第2次gc
删除的虛引用 obj 為: java.lang.ref.PhantomReference@6f94fa3e
但是擷取虛引用的對象 obj.get() 為: null
           

在經過一次GC之後, 系統找到了垃圾對象, 并調用finalize()方法回收記憶體, 但沒有立即将虛引用對象加入回收隊列.

第二次GC時, 該對象真正被GC清除, 此時虛引用對象被加入虛引用隊列.

(2) 虛引用的使用場景:

虛引用的最大作用在于跟蹤對象回收, 清理被銷毀對象的相關資源.

通常當對象不被使用時, 重載該對象的類的

finalize()

方法可以回收對象的資源. 但是如果使用不慎, 會使得對象複活. 比如這樣重寫

finalize()

方法:

public class Test {
    private static Test obj;
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        obj = this;
    }
}
           

建立Test對象:

obj = new Test();

, 然後令

obj = null;

, 之後調用

System.gc()

企圖銷毀該對象.

但是很抱歉, 不管你調用多少次

System.gc()

都沒有用, 除非再次

obj = null;

才能回收對象.

原因: JVM對每一個對象最多隻執行一次被重寫的

finalize()

方法, 示例代碼中, 在

super.finalize()

之後又對obj進行了指派, 使得obj又複活了, 它重寫的finalize()方法不會被調用第二次.

(3) 通過虛引用清理對象

上面的小片段說明重寫

finalize()

方法并不是很靠譜, 我們可以使用虛引用來清理對象所占用的資源. 修改代碼如下:

package com.healchow.java.detail;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class PhantomRefTest2 {
    private static ReferenceQueue<MyObject> phantomQueue = new ReferenceQueue<>();
    private static Map<Reference<MyObject>, String> resourceMap = new HashMap<>();

    private static class MyObject {
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("MyObject's finalize called");
        }
        @Override
        public String toString() {
            return "I am MyObject";
        }
    }

    private static class CheckRefQueue implements Runnable {
        Reference<MyObject> refObj = null;

        @Override
        public void run() {
            try {
                refObj = (Reference<MyObject>) phantomQueue.remove();
                // 從資源Map中移除弱引用對象, 即手動釋放資源
                Object value = resourceMap.get(refObj);
                System.out.println("clean resource: " + value);
                resourceMap.remove(refObj);

                System.out.println("删除的虛引用為: " + refObj);
                System.out.println("擷取虛引用的對象 obj.get() 為: " + refObj.get());
                System.exit(0);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyObject object = new MyObject();
        Reference<MyObject> phantomRef = new PhantomReference<>(object, phantomQueue);
        System.out.println("建立的虛引用為: " + phantomRef);
        new Thread(new CheckRefQueue()).start();
        // 将建立的虛引用對象存入資源Map
        resourceMap.put(phantomRef, "Some Resources");

        object = null;
        TimeUnit.SECONDS.sleep(1);
        int i = 1;
        while (true) {
            System.out.println("第" + i++ + "次gc");
            System.gc();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}
           

運作結果:

建立的虛引用為: java.lang.ref.PhantomReference@6f94fa3e
第1次gc
MyObject's finalize called
第2次gc
clean resource: Some Resources
删除的虛引用 obj 為: java.lang.ref.PhantomReference@6f94fa3e
但是擷取虛引用的對象 obj.get() 為: null
           

《Java程式性能優化——讓你的Java程式更快、更穩定》葛一鳴 等編著。

《Java堆記憶體http://blog.csdn.net/u013256816/article/details/50764532》

本文版權歸原作者所有,如有侵權,請聯系部落客,定當立即删除。

感謝閱讀,公衆号 「瘦風的南牆」 ,手機端閱讀更佳,還有其他福利和心得輸出,歡迎掃碼關注🤝