1. 程式人生 > 其它 >一文帶你理解jvm之強軟弱虛引用

一文帶你理解jvm之強軟弱虛引用

強軟弱虛引用

在java中,除了基本資料型別的變數外,其他所有的變數都是引用型別,指向堆上各種不同的物件。

在jvm中,除了我們常用的強引用外,還有軟引用、弱引用、虛引用,這四種引用型別的生命週期與jvm的垃圾回收過程息息相關。

那麼這四種引用型別有什麼區別?具體使用場景是什麼?

所有引用型別,都是抽象類java.lang.ref.Reference的子類,這個類的主要方法為get()方法:

    public T get() {
        return this.referent;
    }

除了虛引用(因為get永遠返回null),如果物件還沒有被銷燬,都可以通過get方法獲取原有物件。這意味著,利用軟引用和弱引用,我們可以將訪問到的物件,重新指向強引用,也就是人為的改變了物件的可達性狀態!

強引用

強引用(Strong references)就是直接new一個普通物件,表示一種比較強的引用關係,只要還有強引用物件指向一個物件,那麼表示這個物件還活著,垃圾收集器寧可丟擲OOM異常,也不會回收這個物件。

例如下面的引用u就是一個強引用。

public class StrongReferenceDemo {
    public static void main(String[] args) throws IOException {
        User u = new User();
        System.out.println(u);
        u = null;
        System.gc();
        System.in.read();
    }
}
public class User {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("call User finalize() method");
    }
}

上面的User物件重寫了父類Object的finalize(),在GC準備釋放物件所佔用的記憶體空間之前,它將首先呼叫finalize()方法。

在Java中,由於GC的自動回收機制,因而並不能保證finalize方法會被及時地執行(垃圾物件的回收時機具有不確定性),也不能保證它們會被執行(程式由始至終都未觸發垃圾回收),所以finalize不推薦使用,這裡只是為了演示垃圾回收的過程。

另外finalize()最多隻會被呼叫一次,也就是隻能利用finalize()為物件續命一次。

軟引用

軟引用用於儲存一些可有可無的東西,例如快取,當系統記憶體充足時,這些物件不會被回收,當系統記憶體不足時也是GC時才會回收這些物件,如果回收完這些物件後記憶體還是不足,就會丟擲OOM異常。

// vm args: -Xmx36m -XX:+PrintGCDetails
public class SoftReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        SoftReference<User> softReference = new SoftReference<>(new User()); // 軟引用
        System.out.println(softReference.get());
        System.gc(); 
        TimeUnit.SECONDS.sleep(3); // wait gc thread run
        System.out.println(softReference.get()); // User物件不會被回收
        byte[] bytes = new byte[1024 * 1024 * 10]; // 分配一個大物件使得堆空間不足,軟引用物件會在OOM之前先被回收
        System.out.println(softReference.get());
    }
}

在上面的例子中,第一次發生gc時,User物件不會被回收,第二次發生gc時由於堆空間不足,會先回收軟引用的物件,回收完了還是空間不足,最後丟擲OOM異常。

弱引用

弱引用(WeakReference)並不能使物件豁免垃圾回收,僅僅是提供一種訪問在弱引用狀態下物件的途徑。只要發生gc,弱引用物件就會被回收。ThreadLocal中就使用了WeakReference來避免記憶體洩漏。

public class WeakReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        WeakReference<User> weakReference = new WeakReference<>(new User());
        System.out.println(weakReference.get());
        System.gc();
        TimeUnit.SECONDS.sleep(3); // wait gc thread run
        System.out.println(weakReference.get()); // null
    }
}

上面的例子只要發生gc,User物件就會被垃圾收集器回收。

虛引用

虛引用必須和引用佇列(ReferenceQueue)聯合使用。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

虛引用主要用來跟蹤物件被垃圾回收的活動。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件之前,把這個虛引用加入到與之關聯的引用佇列中。程式如果發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。

// vm args: -Xms4m -XX:+PrintGC
public class PhantomReferenceDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        ReferenceQueue<User> referenceQueue = new ReferenceQueue<>(); // 引用佇列
        PhantomReference<User> phantomReference = new PhantomReference<>(new User(), referenceQueue); // 虛引用
        System.out.println(phantomReference.get()); // null
        new Thread(() -> {
            while (true) {
                Reference<? extends User> poll = referenceQueue.poll();
                if (poll != null) {
                    System.out.println("--- 虛引用物件被jvm回收了 ---- " + poll);
                    System.out.println("--- 回收物件 ---- " + poll.get()); // null
                }
            }
        }).start();
        TimeUnit.SECONDS.sleep(1);
        System.gc();
        System.in.read();
    }
    private static class User {
        private int[] bytes = new int[1024 * 1024 * 5];
        @Override
        protected void finalize() throws Throwable {
            System.out.println("call User finalize() method");
        }
    }
}

實際上,虛引用的get()方法總是返回null。

基於虛引用,有一個更加優雅的實現方式,那就是Cleaner,可以用來替代Object類的finalizer方法,在DirectByteBuffer中用來回收堆外記憶體。