Java中的引用--強軟弱虛
強引用
Object object = new Object()
,這個object就是一個強引用。如果一個物件具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError異常,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。
軟引用(SoftReference)
如果一個物件只具有軟引用,那就類似於可有可物的生活用品。如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的快取記憶體。 軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。
public class TestSoftReference {
public static void main(String[] args) throws InterruptedException {
ReferenceQueue<Object> queue = new ReferenceQueue<>();
// m強引用指向softReference,softReference軟指向byte[]
SoftReference<byte[]> m = new SoftReference<>(new byte[1024 * 1024 * 10],queue);
// 列印結果:[B@1e643faf
System.out.println(m.get());
System.gc();
Thread.sleep(1000);
// 列印結果:[B@1e643faf 表示沒有被垃圾回收
System.out.println(m.get());
// 給出一個強引用
byte[] bytes = new byte[1024 * 1024 * 15];
// 不規定最大堆記憶體大小時,列印結果:[B@1e643faf
// 指定最大堆記憶體-Xmx20M時,列印輸出null
System.out.println(m.get());
//列印結果:java.lang.ref.SoftReference@6e8dacdf
System.out.println(queue.poll());
}
}
不指定引數,輸出結果
[B@1e643faf
[B@1e643faf
[B@1e643faf
null
指定引數-Xmx20M,輸出結果
[B@1e643faf
[B@1e643faf
null
java.lang.ref.SoftReference@6e8dacdf
弱引用(WeakReference)
如果一個物件只具有弱引用,那就類似於可有可物的生活用品。弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它 所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒, 因此不一定會很快發現那些只具有弱引用的物件。 弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。
public class TestWeakReference {
public static void main(String[] args) {
WeakReference<byte[]> m = new WeakReference<>(new byte[1024*1024*10]);
System.out.println(m.get());
System.gc();
System.out.println(m.get());
}
}
有垃圾回收直接回收,列印結果:
[B@1e643faf
null
虛引用(PhantomReference)
顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。 虛引用主要用來跟蹤物件被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。程式如果發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。 主要用在管理對外記憶體
ThreadLocal
ThreadLocal提供執行緒區域性變數。這些變數與普通變數不同,因為每個執行緒都有其自己的、獨立初始化的變數副本。ThreadLocal例項通常是類中的私有靜態變數,並將它與執行緒的狀態繫結(例如,使用者ID或事務ID)。
簡單案例:
public class TestThreadLocal {
private static final AtomicInteger nextId = new AtomicInteger(0);
private static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(nextId::getAndIncrement);
public static int get() {
return threadId.get();
}
public static void main(String[] args) {
new Thread(()->{
System.out.println(TestThreadLocal.get()); // 0
try {
Thread.sleep(1000);
System.out.println(TestThreadLocal.get()); // 0
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()->{System.out.println(TestThreadLocal.get());}).start(); // 1
}
}
這裡通過ThreadLocal
物件threadId
為每一個呼叫TestThreadLocal.get()
方法的執行緒賦予一個執行緒Id,第4行通過ThreadLocal.withInitial(nextId::getAndIncrement)
得到ThreadLocal
的子類SuppliedThreadLocal
物件,SuppliedThreadLocal
物件複寫了initialValue
方法。
@Override
protected T initialValue() {
return supplier.get();
}
具體細節下面再談。先看看main
方法,其中啟動了兩個執行緒,可以看到每個執行緒通過呼叫TestThreadLocal.get()
得到獨有的Id。接下來分析ThreadLocal
的主要方法。
set方法
原始碼:
public void set(T value) {
// 獲取當前執行緒
Thread t = Thread.currentThread();
// 得到執行緒的threadLocals屬性,是ThreadLocalMap物件,其中k為這個ThreadLocal物件,v為value
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
從中可以看到ThreadLocalMap
物件是實現功能的關鍵,整體思路和HashMap
相似,具體程式碼就不細看了,有興趣可以自己點進去看,接下來只講述其中的關鍵點。ThreadLocalMap
維護了一個Entry
陣列,對ThreadLocal
物件的HashCode進行處理後作為index將Entry
物件新增到陣列中。接下來就是重中之重,Entry
類:
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
,他的弱引用指向了ThreadLocal
物件,並且擁有屬性value
。看下來可能有點暈了,給出一個圖方便理解
可以理解為每一個Thread
都有一個ThreadLocalMap
屬性,其中key為弱引用指向ThreadLocal
,value為強引用指向傳入的物件。
為什麼要用弱引用作為key?
如果key為強引用,當我們現在將ThreadLocal
的引用指向為null
,但是每個執行緒中有自己獨立ThreadLocalMap
,還會一直持有該物件,所以ThreadLocal
物件不會被回收,會發生記憶體洩漏問題。如果key為弱引用,當我們現在將ThreadLocal
的引用指向為null
時,執行緒中獨立的ThreadLocalMap
中的ThreadLocal
物件會被回收。
還是有記憶體洩漏?
但是會發現就算是key被回收了,value也仍然被Entry
中的value
強引用指著不會被回收,依然會發生記憶體洩漏,所以在不用value的時候應該主動呼叫ThreadLocal
物件的remove
方法來移除。
remove方法
原始碼:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear(); // 清理弱引用
expungeStaleEntry(i);
return;
}
}
}
expungeStaleEntry(i);
將Entry
陣列的第i個entry
物件的value
置為null
,然後將這個enrty
物件置為null
,最後進行rehash。
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();
}
在get方法中,通過getMap()
獲得當前Thread
物件的threadLocals
屬性。在沒有呼叫set方法之前,threadLocals
屬性為null
,所以會呼叫setInitialValue()
:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
可以看到,直接呼叫initialValue()
方法得到value,然後設定並返回value,這就是前面為什麼重寫initialValue()
方法。通過重寫initialValue()
方法,給頂一個初始值,這樣在沒有呼叫set方法之前呼叫get方法就會從initialValue()
中得到一個初始值。