作者: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");
}
}
}
如果執行了一次GC
,reference.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
,就可以同時保證執行緒安全和效能需求。
場景二:每個執行緒內部儲存全域性變數,避免傳參麻煩:假設一個執行緒的作用是拿到前端使用者資訊,逐層執行Service1
,Service2
,Service3
,Service4
層的業務邏輯,其中每個業務層都會用到使用者資訊,此時一個解決辦法就是將User
資訊物件作為引數層層傳遞,但是這樣會導致程式碼冗餘且不利於維護。此時可以將User
資訊物件放入當前執行緒的Threadlocal
中,就變成了全域性變數,在每一層業務層中,需要使用的時候直接從Threadlocal
中獲取即可。
場景三:Spring
的宣告式事務,資料庫連線寫在配置檔案,多個方法可以支援一個完整的事務,保證多個方法是用的同一個資料庫連線(其實就是放在ThreadLocal
裡面)
瞭解了ThreadLocal
簡要介紹以後,我們可以深入理解一下ThreadLocal
的一個內部原理,前面提到,ThreadLocal
的set
方法實際上是往當前執行緒的一個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
,當我們檢測到這個虛引用被垃圾回收器回收的時候,可以做出相應的處理,去回收堆外記憶體。