天天看點

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:[[email protected]

before gc ref:[email protected]

[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:[email protected]

當第二次申請記憶體的時候,發現記憶體不夠,則會檢查記憶體中是否有軟引用,有的話就會回收。

如果是第二次申請的仍然是軟引用的資料,結果是一樣的,将會回收舊的空間。

那麼什麼時候會發生回收的事件呢?每次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:[[email protected]

before gc ref:[email protected]

[GC (System.gc()) 33091K->31536K(49152K), 0.0011518 secs]

[Full GC (System.gc()) 31536K->31358K(49152K), 0.0082917 secs]

after gc data:[[email protected]

after gc ref:[email protected]

可以看到,即便是發生了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);
    }
           

執行結果如下:

[[email protected]

time:0–[[email protected]

[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就會把那個虛引用的變量放到隊列中,表示對象被回收。

畫個圖,如下:

java的4種引用類型及應用場景強引用軟引用弱引用虛引用

當上面的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回收了 ---- [email protected]

使用場景

使用虛引用的目的就是為了得知對象被GC的時機,是以可以利用虛引用來進行銷毀前的一些操作,比如說資源釋放等。這個虛引用對于對象而言完全是無感覺的,有沒有完全一樣,但是對于虛引用的使用者而言,就像是待觀察的對象的把脈線,可以通過它來觀察對象是否已經被回收,進而進行相應的處理。

事實上,虛引用有一個很重要的用途就是用來做堆外記憶體的釋放,DirectByteBuffer就是通過虛引用來實作堆外記憶體的釋放的。