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

Grey Zeng 發表於 2021-10-07
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