Java中的引用類型和使用場景

作者:Grey

原文地址:Java中的引用類型和使用場景

Java中的引用類型有哪幾種?

Java中的引用類型分成強引用軟引用, 弱引用, 虛引用

強引用

沒有引用指向這個對象,垃圾回收會回收

package git.snippets.juc;

import java.io.IOException;

public class NormalRef {
    public static void main(String[] args) throws IOException {
        M m = new M();
        m = null;
        System.gc();
        System.in.read();
    }
    static class M {
        M() {}
        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalized");
        }
    }
}

軟引用

當有一個對象被一個軟引用所指向的時候,只有系統記憶體不夠用的時候,才會被回收,可以用做快取(比如快取大圖片)

示例如下程式碼:註:執行以下方法的時候,需要把VM options設置為-Xms20M -Xmx20M

package git.snippets.juc;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.concurrent.TimeUnit;

/**
 * heap將裝不下,這時候系統會垃圾回收,先回收一次,如果不夠,會把軟引用幹掉
 * 軟引用,適合做快取
 * 示例需要把Vm options設置為:-Xms20M -Xmx20M
 */
public class SoftRef {
    public static void main(String[] args) throws IOException {
        SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024 * 10]);
        System.out.println(reference.get());
        System.gc();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(reference.get());
        byte[] bytes = new byte[1024 * 1024 * 10];
        System.out.println(reference.get());
        System.in.read();
    }
}

上述程式碼在第一次執行System.out.println(reference.get())時候,由於堆的最大最小值都是20M,而我們分配的byte數組是10M,沒有超過最大堆記憶體,所以執行垃圾回收,軟引用不被回收,後續又調用了byte[] bytes = new byte[1024 * 1024 * 10];再次分配了10M記憶體,此時堆記憶體已經超過設置的最大值,會進行回收,所以最後一步的System.out.println(reference.get());無法get到數據。

弱引用

只要垃圾回收,就會回收。如果有一個強引用指向弱引用中的這個對象,如果這個強引用消失,這個對象就應該被回收。一般用在容器裡面。

程式碼示例如下:

package git.snippets.juc;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
 * 弱引用遭到gc就會回收
 * ThreadLocal應用,快取應用,WeakHashMap
 */
public class WeakRef {
    public static void main(String[] args) {
        WeakReference<T> reference = new WeakReference<>(new T());
        System.out.println(reference.get());
        System.gc();
        System.out.println(reference.get());
    }
    static class T {
        T() {}
        @Override
        protected void finalize() {
            System.out.println("finalized");
        }
    }
}

如果執行了一次GCreference.get() 獲取到的值即為空。

弱引用的使用場景

弱引用的一個典型應用場景就是ThreadLocal,以下是ThreadLocal的的簡要介紹

set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocalMap是當前執行緒的一個成員變數,所以,其他執行緒無法讀取當前執行緒設置的ThreadLocal值。

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal的主要應用場景

場景一:每個執行緒需要一個獨享的對象:假設有100個執行緒都需要用到SimpleDateFormat類來處理日期格式,如果共用一個SimpleDateFormat,就會出現執行緒安全問題,導致數據出錯,如果加鎖,就會降低性能,此時使用ThreadLocal,給每個執行緒保存一份自己的本地SimpleDateFormat,就可以同時保證執行緒安全和性能需求。

場景二:每個執行緒內部保存全局變數,避免傳參麻煩:假設一個執行緒的作用是拿到前端用戶資訊,逐層執行Service1Service2Service3Service4層的業務邏輯,其中每個業務層都會用到用戶資訊,此時一個解決辦法就是將User資訊對象作為參數層層傳遞,但是這樣會導致程式碼冗餘且不利於維護。此時可以將User資訊對象放入當前執行緒的Threadlocal中,就變成了全局變數,在每一層業務層中,需要使用的時候直接從Threadlocal中獲取即可。

場景三:Spring的聲明式事務,資料庫連接寫在配置文件,多個方法可以支援一個完整的事務,保證多個方法是用的同一個資料庫連接(其實就是放在ThreadLocal裡面)

了解了ThreadLocal簡要介紹以後,我們可以深入理解一下ThreadLocal的一個內部原理,前面提到,ThreadLocalset方法實際上是往當前執行緒的一個threadLocals表中插入一條記錄,而這個表中的記錄都存在一個Entry對象中,這個對象有一個key和一個value,key就是當前執行緒的ThreadLocal對象。

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

這個Entry對象繼承了WeakReference, 且構造函數調用了super(k), 所以Entry中的key是通過一個弱引用指向的ThreadLocal,所以,我們在主方法中調用

ThreadLocal<Object> tl = new ThreadLocal<>();

tl是通過強引用指向這個ThreadLocal對象。

當前執行緒的threadLocalMap中的key是通過弱引用指向ThreadLocal對象,這樣就可以保證,在tl指向空以後,這個ThreadLocal會被回收,否則,如果threadLocalMap中的key是強引用指向ThreadLocal對象話,這個ThreadLocal對象永遠不會被回收。就會導致記憶體泄漏。

但是,即便key用弱引用指向ThreadLocal對象,key值被回收後,Entry中的value值就無法被訪問到了,且value是通過強引用關聯,所以,也會導致記憶體泄漏,所以,每次在ThreadLocal中的對象不用了,記得要調用remove方法,把對應的value也給清掉。

虛引用

用於管理堆外記憶體回收

虛引用關聯了一個對象,以及一個隊列,只要垃圾回收,虛引用就被回收,一旦虛引用被回收,虛引用會被裝到這個隊列,並會收到一個通知(如果有值入隊列,會得到一個通知)所以,如果想知道虛引用何時被回收,就只需要不斷監控這個隊列是否有元素加入進來了。

虛引用裡面關聯的對象用get方法是無法獲取的。

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;

// 配置 -Xms20M -Xmx20M
public class PhantomRef {
    private static final List<Object> LIST = new LinkedList<>();
    private static final ReferenceQueue<P> QUEUE = new ReferenceQueue<>();


    public static void main(String[] args) {
        PhantomReference<P> phantomReference = new PhantomReference<>(new P(), QUEUE);
        new Thread(() -> {
            while (true) {
                LIST.add(new byte[1024 * 1024]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println(phantomReference.get());
            }
        }).start();

        new Thread(() -> {
            while (true) {
                Reference<? extends P> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虛引用對象被jvm回收了 ---- " + poll);
                }
            }
        }).start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    static class P {
        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalized");
        }
    }
}

虛引用的應用場景

JDK的NIO包中有一個DirectByteBuffer, 這個buffer指向的是堆外記憶體,所以當這個buffer設置為空的時候,Java的垃圾回收無法回收,所以,可以用虛引用來管理這個buffer,當我們檢測到這個虛引用被垃圾回收器回收的時候,可以做出相應的處理,去回收堆外記憶體。

源碼

juc