java的4種引用類型及應用場景

Java有4種引用類型,分別是強引用、軟引用、弱引用和虛引用。
主要是爲了根據場景來控制不同的回收時機。

強引用

強引用(Strong Reference)是最普通的引用,以=進行賦值,例如String s="ni hao"中的s就是一個強引用。
特性: 只要有引用存在,那麼堆中數據不會被回收,哪怕是OOM。
代碼示例(-Xmx50m -verbose:gc):

public static void main(String[] args) throws InterruptedException {
    //首先設置-Xmx50m
    List list = new ArrayList();
    for (int i = 0; i < 10; i++) {
        list.add(new byte[1024 * 1024 * 10]);//10M
        TimeUnit.SECONDS.sleep(1);
        System.out.println("time:" + i);
    }
}

運行的結果是撐爆內存,拋出OOM異常。

軟引用

軟引用(SoftReference)是一種比強引用弱的引用。
特性: 在分配堆內存的時候,如果空間不足,就會將堆中的軟引用的數據空間回收。
代碼示例(-Xmx50m -verbose:gc):

public class SoftRefer {
    public static void main(String[] args) throws InterruptedException {
        SoftReference softReference = new SoftReference(new byte[1024 * 1024 * 30]);//30M
        System.out.println("before gc data:" + softReference.get());
        System.out.println("before gc ref:" + softReference);
        byte[] bytes = new byte[1024 * 1024 * 30];
        System.out.println("after gc data:" + softReference.get());
        System.out.println("after gc ref:" + softReference);
        TimeUnit.SECONDS.sleep(10);
    }
}

運行結果如下:

before gc data:[B@4554617c
before gc ref:java.lang.ref.SoftReference@74a14482
[GC (Allocation Failure) 33091K->31568K(49152K), 0.0010995 secs]
[Full GC (Ergonomics) 31568K->31358K(49152K), 0.0080070 secs]
[GC (Allocation Failure) 31358K->31358K(49152K), 0.0007548 secs]
[Full GC (Allocation Failure) 31358K->620K(37376K), 0.0082833 secs]
after gc data:null
after gc ref:java.lang.ref.SoftReference@74a14482

當第二次申請內存的時候,發現內存不夠,則會檢查內存中是否有軟引用,有的話就會回收。
如果是第二次申請的仍然是軟引用的數據,結果是一樣的,將會回收舊的空間。
那麼什麼時候會發生回收的事件呢?每次gc就會回收嗎?
將代碼修改一下,增加一個gc的操作,如下:

public class SoftRefer {
    public static void main(String[] args) throws InterruptedException {
        SoftReference softReference = new SoftReference(new byte[1024 * 1024 * 30]);//30M
        System.out.println("before gc data:" + softReference.get());
        System.out.println("before gc ref:" + softReference);
        System.gc();
        System.out.println("after gc data:" + softReference.get());
        System.out.println("after gc ref:" + softReference);
        TimeUnit.SECONDS.sleep(10);
    }
}

執行結果如下:

before gc data:[B@4554617c
before gc ref:java.lang.ref.SoftReference@74a14482
[GC (System.gc()) 33091K->31536K(49152K), 0.0011518 secs]
[Full GC (System.gc()) 31536K->31358K(49152K), 0.0082917 secs]
after gc data:[B@4554617c
after gc ref:java.lang.ref.SoftReference@74a14482

可以看到,即便是發生了Full gc,也沒有回收掉上面的軟引用。那OOM會不會回收呢?應該會的,上面的第一個例子裏面,即便是沒有發生OOM,也是會回收的,因爲分配空間不足了,不過還是試一試吧。
代碼如下(-Xmx50m -verbose:gc):

    public static void main(String[] args) throws InterruptedException {
        //首先設置-Xmx50m
        SoftReference softReference = new SoftReference(new byte[1024 * 1024 * 30]);//30M
        System.out.println(softReference.get());
        System.out.println(softReference);
        List list = new ArrayList();
        while (true) {
            list.add(new byte[1024 * 1024 * 5]);
            System.out.println(softReference.get());
        }
    }

結果如料想的那樣,到不了OOM的時候就會被回收了。jdk8的SoftReference有如下的註釋:

Soft reference objects, which are cleared at the discretion of the
garbage collector in response to memory demand. Soft references are
most often used to implement memory-sensitive caches. Suppose that the
garbage collector determines at a certain point in time that an object
is softly reachable. At that time it may choose to clear atomically
all soft references to that object and all soft references to any
other softly-reachable objects from which that object is reachable
through a chain of strong references. At the same time or at some
later time it will enqueue those newly-cleared soft references that
are registered with reference queues.

All soft references to softly-reachable objects are guaranteed to have
been cleared before the virtual machine throws an OutOfMemoryError.
Otherwise no constraints are placed upon the time at which a soft
reference will be cleared or the order in which a set of such
references to different objects will be cleared. Virtual machine
implementations are, however, encouraged to bias against clearing
recently-created or recently-used soft references.

Direct instances of this class may be used to implement simple caches;
this class or derived subclasses may also be used in larger data
structures to implement more sophisticated caches. As long as the
referent of a soft reference is strongly reachable, that is, is
actually in use, the soft reference will not be cleared. Thus a
sophisticated cache can, for example, prevent its most recently used
entries from being discarded by keeping strong referents to those
entries, leaving the remaining entries to be discarded at the
discretion of the garbage collector.

所以,回收時機可以這樣描述:

  1. 當發生GC時,虛擬機可能會回收SoftReference對象所指向的軟引用,如果空間足夠,就不會回收,還要取決於該軟引用是否是新創建或近期使用過。
  2. 在虛擬機拋出OutOfMemoryError之前,所有軟引用對象都會被回收。

使用場景
可以是在排除過期數據的情況下。JDK中有個類叫ResourceBundle,內部會使用ConcurrentMap緩存ResourceBundle對象,這裏就是使用的軟引用機制。

弱引用

弱引用(WeakReference)是比軟引用更弱的一種引用。
特性: 只要觸發了gc(包括Allocation Failure類型的gc),無論內存空間是否充足,都會將堆中的弱引用的數據空間回收。
示例代碼(-Xmx50m -verbose:gc):

    public static void main(String[] args) throws InterruptedException {
        //首先設置-Xmx50m
        WeakReference weakReference = new WeakReference(new byte[1024 * 1024 * 5]);//5M
        System.out.println(weakReference.get());

        List list = new ArrayList();
        for (int i = 0; i < 10; i++) {
            list.add(new byte[1024 * 1024 * 5]);
            System.out.println("time:" + i + "==" + weakReference.get());
        }

        TimeUnit.SECONDS.sleep(5);
    }

執行結果如下:

[B@4554617c
time:0–[B@4554617c
[GC (Allocation Failure) 12611K->5968K(49152K), 0.0042473 secs]
time:1–null
time:2–null
…(略)

可以從上面看到,哪怕是在年輕代空間不足的時候,也會把弱引用回收掉。
使用場景
個人理解,是在較爲複雜的數據結構中,爲了避免內存泄露而使用的一種引用方式。
下面說一下ThreadLocal中對弱引用的使用。

ThreadLocal中的弱引用

ThreadLocal的存在是提供了一種線程內全局的上下文容器,類似Spring中的Context,只不過使用範圍是在當前線程內。
它可以簡化同一個線程內多個方法直接參數的重複傳遞,隔離其他線程的干擾。
原理是把ThreadLocal變量以弱key的形式存放在java.lang.ThreadLocal.ThreadLocalMap中。
java.lang.ThreadLocal.ThreadLocalMap.Entry就是繼承了WeakReference,把ThreadLocal作爲一種弱引用存在於map中的key中,一旦ThreadLocal變量的引用被回收或者被置爲null值的時候,JVM就會將數據中的key也回收掉並置爲null值。
可以寫個例子來證明上面的結論。
首先爲了看到回收結果,我繼承了一下ThreadLocal,打了個日誌,代碼如下:

public class WeakRefer {
    public static void main(String[] args) throws InterruptedException {
        saveSomething("something");
        System.gc();
        TimeUnit.SECONDS.sleep(5);
    }

    private static void saveSomething(String str) {
        MyThreadLocal<String> threadLocal = new MyThreadLocal<>();
        threadLocal.set(str);
    }
}

class MyThreadLocal<T> extends ThreadLocal<T> {

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("============finalize");
    }
}

執行結果如下:

[GC (System.gc()) 2115K->848K(49152K), 0.0014758 secs]
[Full GC (System.gc()) 848K->612K(49152K), 0.0076629 secs]
============finalize

可以看到,當threadLocal引用消失的時候,數組中的弱引用也被回收了。
如果不使用弱引用,那麼threadLocal變量就會一直存在於java.lang.ThreadLocal.ThreadLocalMap中,直到線程停止運行。
這樣的話,就有可能會有內存泄露的風險。

虛引用

虛引用是使用PhantomReference創建的引用,虛引用也稱爲幽靈引用或者幻影引用,是所有引用類型中最弱的一個。
一個對象是否有虛引用的存在,完全不會對其生命週期構成影響,也無法通過虛引用獲得一個對象實例。
虛引用只有一個含有隊列的構造函數,也就是說,虛引用必須和隊列同時使用,換句話說,虛引用是通過隊列來實現它的價值的。
當虛引用所指向的那塊內存被回收之後,JVM就會把那個虛引用的變量放到隊列中,表示對象被回收。
畫個圖,如下:
在這裏插入圖片描述
當上面的data數據塊被回收之後,JVM就會把PhantomRef這個引用放入到queue中。
簡單寫個demo,如下所示(-verbose:gc):

public class PhantomRefer {
    private static final ReferenceQueue<MyUsefulObj> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        MyUsefulObj obj = new MyUsefulObj();
        PhantomReference<MyUsefulObj> reference = new PhantomReference<>(obj, QUEUE);
        monitorQueue();
        obj = null;
        System.gc();
    }

    private static void monitorQueue() {
        // 這個線程不斷讀取引用隊列,當弱引用指向的對象唄回收時,該引用就會被加入到引用隊列中
        new Thread(() -> {
            while (true) {
                Reference<? extends MyUsefulObj> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虛引用對象被jvm回收了 ---- " + poll);
                }
            }
        }).start();
    }

}

class MyUsefulObj {
    public void doSomething() {
        //一些重要的事情
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("============finalize");
    }
}

我所期望的結果是打印出“虛引用對象被jvm回收了”,但是實際上並沒有,輸出如下:

[GC (System.gc()) 6506K->1024K(249344K), 0.0014907 secs]
[Full GC (System.gc()) 1024K->838K(249344K), 0.0090643 secs]
============finalize

明明執行了finalize()方法,那爲什麼隊列中沒有取到值呢?是對象實際上沒有被回收嗎?還是因爲已經回收了但是JVM沒有把引用放到隊列中?
這個時候就涉及finalize方法的特性了。
finalize()是在回收之前被JVM調用的一個方法,但並不意味着調用了該方法之後就一定會被回收,也可能被救贖。
在上面這個例子當中,主要是因爲複寫了finalize()方法,導致了MyUsefulObj對象被延遲迴收了。
當我把複寫finalize的方法刪掉之後,再次執行,結果就是預期的那樣了,如下:

[GC (System.gc()) 6506K->1024K(249344K), 0.0010478 secs]
[Full GC (System.gc()) 1024K->838K(249344K), 0.0061029 secs]
— 虛引用對象被jvm回收了 ---- java.lang.ref.PhantomReference@6018d1f6

使用場景
使用虛引用的目的就是爲了得知對象被GC的時機,所以可以利用虛引用來進行銷燬前的一些操作,比如說資源釋放等。這個虛引用對於對象而言完全是無感知的,有沒有完全一樣,但是對於虛引用的使用者而言,就像是待觀察的對象的把脈線,可以通過它來觀察對象是否已經被回收,從而進行相應的處理。
事實上,虛引用有一個很重要的用途就是用來做堆外內存的釋放,DirectByteBuffer就是通過虛引用來實現堆外內存的釋放的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章