JDK源碼 Java Reference

更多請移步: 我的博客

JDK源碼 Java的四種Reference

之前探討過一次JAVA的FinalReference,這次我們來看下java.lang.ref包下對應的其他三種引用。

走近引用

Reference和ReferenceQueue在使用中一定是結伴出現的,當一個Reference確定要被GC回收,GC便會把Reference加入到與之關聯的ReferenceQueue中。注意:在Reference的構造方法中,我們可以傳入一個註冊隊列ReferenceQueue,這個隊列我們稍後會具體看,需要主要的是,這個隊列需要單獨的線程去做消費,否則會存在OOM的隱患。

這些引用可用來實現不同的緩存類型(內存敏感和內存不敏感),大名鼎鼎的Guava cache就是基於引用的這些特性來實現高速本地緩存。

StrongReference(強引用)

我們平時開發中new一個對象出來,這種引用便是強引用。 JVM 系統採用 Finalizer 來管理每個強引用對象 , 並將其被標記要清理時加入 ReferenceQueue, 並逐一調用該對象的 finalize() 方法。具體詳見我的前一篇博客:JDK源碼 FinalReference

SoftReference(軟引用)

當內存足夠的時候,軟引用所指向的對象沒有其他強引用指向的話,GC的時候並不會被回收,當且只當內存不夠時纔會被GC回收(調用finalize方法)。強度僅次於強引用。GC回收前,會將那些已經向引用隊列註冊的新清除的軟引用加入隊列。

public class ClassSoft {

    public static class Referred {
        /**
         * 不是必須實現,和Strong不同。
         * 實現該方法是爲了追蹤GC,
         * 實現後也會被當作Finalizer
         * @throws Throwable
         */
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Referred對象被垃圾收集");
        }

        @Override
        public String toString() {
            return "I am Referred";
        }
    }

    public static void collect() throws InterruptedException {
        System.gc();
        Thread.sleep(2000);
    }

    static class CheckRefQueueThread extends Thread{
        @Override
        public void run() {
            Reference<Referred> obj = null;
            try {
                obj = (Reference<Referred>)softQueue.remove();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(obj != null) {
                try {
                    Field referent = Reference.class.getDeclaredField("referent");
                    referent.setAccessible(true);
                    Object result = referent.get(obj);
                    //此處異常可以說明,在被放入隊列之前referent已經被JVM置爲null
                    System.out.println("gc will collect: " + result.getClass() + "@" + result.hashCode());
                } catch (Exception e) {
                    e.printStackTrace();
                }
                System.out.println("Object for SoftReference is " + obj.get());
            }
        }
    }

    //如果我們使用了自定義的註冊隊列,一定要啓動一個線程來處理該隊列
    //JVM只負責像隊列中放入對象,不負責清理
    static ReferenceQueue<Referred> softQueue = new ReferenceQueue<>();

    /**
     * JVM配置
     * -Xms4m -Xmx4m
     * -XX:+PrintGCDetails -Xloggc:/Users/childe/logs/gc-f.log
     * 務必加上該參數,以確定collect方法後GC被執行
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        System.out.println("創建軟引用");

        Referred strong = new Referred();
        SoftReference<Referred> soft = new SoftReference<>(strong,softQueue);
        new CheckRefQueueThread().start();

        ClassSoft.collect();

        System.out.println("切斷強引用");

        strong = null;
        ClassSoft.collect();

        System.out.println("GC之前,軟引用值:" + soft.get().toString());

        System.out.println("開始堆佔用");
        try {
            List<byte[]> bytes = new ArrayList<>();
            while (true) {
                bytes.add(new byte[1024*1024]);
                ClassSoft.collect();
            }
        } catch (OutOfMemoryError e) {
            // 軟引用對象應該在這個之前被收集
            System.out.println("內存溢出...");
        }

        System.out.println("Done");
    }
}

程序輸出如下:

創建軟引用
切斷強引用
GC之前,軟引用值:I am Referred
開始堆佔用
java.lang.NullPointerException
Referred對象被垃圾收集
    at com.cxd.jvm.references.ref.ClassSoft$CheckRefQueueThread.run(ClassSoft.java:54)
Object for SoftReference is null
內存溢出...
Done

我們可以看到,軟引用在GC回收前,調用get方法是可以返回其關聯的實際對象的,當其被GC加入ReferenceQueue前,JVM會將其關聯的對象置爲null。

WeakReference(弱引用)

弱引用指向的對象沒有任何強引用指向的話,GC的時候會進行回收。

/**
 *
 * Created by childe on 2017/3/31.
 */
public class ClassWeak {
    public static class Referred {
        /**
         * 不是必須實現,和Strong不同。
         * 實現該方法是爲了追蹤GC
         * 實現後也會被當作Finalizer
         * @throws Throwable
         */
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Referred對象被垃圾收集");
        }

        @Override
        public String toString() {
            return "I am weak";
        }
    }

    public static void collect() throws InterruptedException {
        System.gc();
        Thread.sleep(2000);
    }

    /**
     * JVM配置
     * -XX:+PrintGCDetails -Xloggc:/Users/childe/logs/gc-f.log
     * 務必加上該參數,以確定collect方法後GC被執行
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        System.out.println("創建一個弱引用");

        Referred strong = new Referred();
        WeakReference<Referred> weak = new WeakReference<>(strong);

        ClassWeak.collect();
        System.out.println("切斷強引用");

        strong = null;

        System.out.println("GC之前,弱引用值:" + weak.get().toString());

        ClassWeak.collect();

        System.out.println("Done");
    }
}

程序輸出如下:

創建一個弱引用
切斷強引用
GC之前,弱引用值:I am weak
Referred對象被垃圾收集
Done
PhantomReference(虛引用)

如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。這個特性,決定了他的get方法每次調用均會返回null。

/**
 * Created by childe on 2017/3/31.
 */
public class ClassPhantom {

    public static class Referred {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("Referred對象被垃圾收集");
        }

        @Override
        public String toString() {
            return "Referredqq";
        }
    }

    public static void collect() throws InterruptedException {
        System.gc();
        Thread.sleep(2000);
    }

    static class CheckRefQueueThread extends Thread{
        @Override
        public void run() {
            Reference<Referred> obj = null;
            try {
                obj = (Reference<Referred>) phantomQueue.remove();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(obj != null) {
                //因爲虛引用的指示對象總是不可到達的,所以此方法總是返回 null
                System.out.println("Object for phantomReference is " + obj.get());
                try {
                    Field referent = Reference.class.getDeclaredField("referent");
                    referent.setAccessible(true);
                    Object result = referent.get(obj);
                    System.out.println("gc will collect: " + result.getClass() + "@" + result.toString());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
    }

    static ReferenceQueue<Referred> phantomQueue = new ReferenceQueue<>();

    /**
     * -Xms4m -Xmx4m
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        System.out.println("創建一個軟引用");

        Referred strong = new Referred();
        PhantomReference<Referred> soft = new PhantomReference<>(strong, phantomQueue);
        new CheckRefQueueThread().start();

        collect();

        System.out.println("切斷強引用");

        strong = null;
        collect();

        System.out.println("開始堆佔用");

        try {
            List<byte[]> bytes = new ArrayList<>();
            while (true) {
                bytes.add(new byte[1024*1024]);
                collect();
            }
        } catch (OutOfMemoryError e) {
            // 軟引用對象應該在這個之前被收集
            System.out.println("內存溢出...");
        }

        System.out.println("Done");
    }
}

輸出如下:

創建一個軟引用
切斷強引用
Referred對象被垃圾收集
開始堆佔用
Object for phantomReference is null
gc will collect: class com.cxd.jvm.references.ref.ClassPhantom$Referred@Referredqq
內存溢出...
Done

引用間的差別

我們注意到虛引用在被加入到ReferenceQueue中後,關聯對象並沒有被置爲null,這點和弱引用及軟引用不同。這也是我開頭說的潛在OOM的最大風險。當然,這種現象只是加速了OOM問題的暴露,並不是根本原因。JVM GC的這個模型可以看作是生產-消費模型,GC是生產者,我們自己起的線程是消費者(Finalizer中JDK自帶線程),當只有生產者時,OOM是遲早的事情。

ReferenceQueue

我們介紹的這四種引用都從java.lang.ref.Reference繼承,Reference是個單向鏈表,ReferenceQueue利用Reference的這個特性來維護先進後出單向隊列(類似棧)。

public abstract class Reference<T> {
    //......
    //引用有4中概念上的狀態:Active、Pending、 Enqueued 、Inactive
    //引用的初始態爲Active或者Pending,它的生命後期爲:(Active || Pending)-> Enqueued -> Inactive
    private T referent;         /* Treated specially by GC 由GC專門處理*/

    ReferenceQueue<? super T> queue; /* Reference 關聯的引用隊列 */

    Reference next; /* 指向下一個引用 */
    //......
}

public class ReferenceQueue<T> {
    //......
    //如果我們構造Reference時,未傳入自定義隊列,默認使用此隊列。
    private static class Null extends ReferenceQueue {
        //入隊操作直接返回
        boolean enqueue(Reference r) {
            return false;
        }
    }

    static ReferenceQueue NULL = new Null();
    static ReferenceQueue ENQUEUED = new Null();

    boolean enqueue(Reference<? extends T> r) { /* Called only by Reference class 只會對Reference類調用該方法 */
        synchronized (r) {
            //以入隊的引用不多次入隊
            if (r.queue == ENQUEUED) return false;
            synchronized (lock) {
                //修改引用入隊狀態爲Enqueued
                r.queue = ENQUEUED;
                //插入對頭
                r.next = (head == null) ? r : head;
                head = r;
                queueLength++;
                if (r instanceof FinalReference) {
                    sun.misc.VM.addFinalRefCount(1);
                }
                //通知等待在鎖上的線程ReferenceQueue.remove()
                lock.notifyAll();
                return true;
            }
        }
    }

    private Reference<? extends T> reallyPoll() {       /* Must hold lock 必須在持有lock鎖的情況下執行,lock由其外層方法獲取 */
        if (head != null) {
            //獲取隊頭
            Reference<? extends T> r = head;
            head = (r.next == r) ? null : r.next;
            //將關聯的隊列置爲NULL,此時r的狀態爲Inactive,處於此狀態的引用不會再發生變化,等待被回收。
            r.queue = NULL;
            r.next = r;
            queueLength--;
            if (r instanceof FinalReference) {
                sun.misc.VM.addFinalRefCount(-1);
            }
            return r;
        }
        return null;
    }
    //......
}

擴展WeakHashMap

JDK中有對引用的具體使用,當我們需要實現一個簡單的本地內存敏感緩存時,可以考慮使用WeakHashMap,此處不再分析其源碼。WeakHashMap的每個Entry都是WeakReference的子類,每次put或者get或者resize擴容時,都會調用WeakHashMap的expungeStaleEntries方法,清除那些被GC加入到ReferenceQueue的Entry。

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