六、強軟弱虛四種引用以及ThreadLocal源碼
強軟弱虛引用
強引用
當我們使用
Object obj = new Object()
建立一個對象時,指向這個對象的引用就稱為強引用。隻要這個引用還指向一個對象,那麼指向的這個對象就不會被垃圾回收器回收。
package com.gouxiazhi.reference;
/**
* 強引用
* @author 趙帥
* @date 2021/1/17
*/
public class ReferenceDemo1 {
public static void main(String[] args) {
// 啟動時設定jvm初始堆和最大堆大小為3M -Xms3M -Xmx3M
// 先建立一個1M的位元組數組,這是強引用
byte[] content = new byte[1024 * 1024];
// 再建立一個2M的位元組數組,因為上面已經建立了1M了,強引用隻要content還指向對象,就不會被垃圾回收器回收。是以會抛出記憶體溢出OOM異常。
// 當打開下面這句代碼時,content不再指向一個對象,那麼上面建立的對象就會被垃圾回收器回收,就可以正常運作了。
// content = null;
byte[] bytes = new byte[2 * 1024 * 1024];
}
}
軟引用
使用軟引用建立的對象,當記憶體不夠時,就會被垃圾回收器回收。
package com.gouxiazhi.reference;
import java.lang.ref.SoftReference;
/**
* 軟引用
* @author 趙帥
* @date 2021/1/17
*/
public class ReferenceDemo2 {
public static void main(String[] args) {
// 啟動時設定jvm初始堆和最大堆大小為3M -Xms3M -Xmx3M
// 使用軟引用建立一個1M的數組。
SoftReference<byte[]> reference = new SoftReference<>(new byte[1024 * 1024]);
System.out.println("reference = " + reference.get());
// 上面使用軟引用建立了一個1M的數組,下面再建立2M的數組時,因為堆記憶體空間不夠,就會調用gc清理掉軟引用的指向的對象。是以不會抛出異常。
// 如果上面不使用軟引用,而使用 byte[] a = new byte[1024*1024];就會抛出記憶體溢出異常。
byte[] bytes = new byte[2 * 1024 * 1024];
// 因為建立上面的對象記憶體不夠,是以軟引用指向的對象已經被回收
System.out.println("reference = " + reference.get());
}
}
弱引用
使用弱引用建立的對象,隻要垃圾回收器看見,就會回收。
package com.gouxiazhi.reference;
import java.lang.ref.WeakReference;
import java.util.Arrays;
/**
* @author 趙帥
* @date 2021/1/17
*/
public class ReferenceDemo3 {
public static void main(String[] args) {
// 弱引用指向的對象,隻要垃圾回收器看見就立馬回收掉。
WeakReference<byte[]> weakReference = new WeakReference<>(new byte[1024 * 1024]);
System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
System.gc();
System.out.println("weakReference = " + Arrays.toString(weakReference.get()));
}
}
虛引用
主要用來管理堆外記憶體等。
package com.gouxiazhi.reference;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* @author 趙帥
* @date 2021/1/17
*/
public class ReferenceDemo4 {
public static class M{
@Override
protected void finalize() throws Throwable {
System.out.println("對象被回收了");
super.finalize();
}
}
public static void main(String[] args) {
ReferenceQueue<M> referenceQueue = new ReferenceQueue<>();
// 虛引用的使用,除了指向一個對象,還需要指定一個引用隊列。
PhantomReference<M> reference = new PhantomReference<>(new M(), referenceQueue);
System.out.println("reference = " + reference);
// 虛引用指向的對象無法被擷取到,弱引用被垃圾回收器看見就會被回收,虛引用比弱引用級别更低
System.out.println("reference = " + reference.get()); // null
// 虛引用一般都是指向一個堆外記憶體,因為垃圾回收器隻能回收堆記憶體,無法管理堆外記憶體.
// 如果使用java管理堆外記憶體。假設M代表着堆外記憶體,那麼當虛引用被回收時,他會将自身放入referenceQueue引用隊列,開啟另一個線程監聽這個隊列,
// 當這個隊列取到内容時,就代表要回收這塊堆外記憶體了,可以執行回收堆外記憶體操作
new Thread(() -> {
// 如果從隊列中取到虛引用,那麼就表示需要回收這個堆外記憶體了。
Reference<? extends M> poll = referenceQueue.poll();
if (poll != null) {
System.out.println("虛引用對象被jvm回收了" + poll);
}
}).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
ArrayList<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[4 * 1024]);
}
}
}
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);
}
可以看到,在set方法中,首先擷取目前線程。然後通過
getMap(t)
擷取了一個ThreadLocalMap對象。然後将要儲存的對象存入了這個Map中,key值就是ThreadLoacl對象本身,value值為要儲存的資料。
然後我們再點開
getMap
方法:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
傳回的是
t.threadLocals
屬性值,而且這個值是
ThreadLocalMap
類型的。檢視
ThreadLocalMap
類:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到
ThreadLocalMap
中存放資料的
Entry
節點繼承自
WeakReference<?>
,是以說
ThreadLocal
底層用的是弱引用,而且在存儲時,Map的key作為弱引用,也就是ThreadLocal對象本身作為弱引用存放,值是強引用存放的。
檢視ThreadLocalMap類的set方法:
private void set(ThreadLocal<?> key, Object value) {
// 擷取存放資料的數組,底層資料結構
Entry[] tab = table;
// 擷取數組的長度
int len = tab.length;
// 計算key要存放的下标
int i = key.threadLocalHashCode & (len-1);
// 從下标i開始周遊數組,如果下标i 有元素了,也就是hash沖突了,那麼就往後插并将下标自增
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 擷取下标為i的entry元素
ThreadLocal<?> k = e.get();
// 如果此位置的key與要儲存的key相同則替換值
if (k == key) {
e.value = value;
return;
}
// 如果key是空的話,說明這個位置的key已經過期被回收,則替換值為新的值。
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果下标i為空,說明這個位置是空的。插入這個位置
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
簡單分析上面整個過程,整個threadlocal存放資料過程甬道圖如下:
![image-20210118143336157](/Users/zhaoshuai/Library/Application Support/typora-user-images/image-20210118143336157.png)
當調用threadLocal的set方法時:
-
- 擷取目前線程的threadLocals屬性。
- 調用threadLocals屬性的set方法。
-
- 擷取threadLocalMap的entry數組。
- 計算目前threadLocal對象的下标。
- 擷取下标的entry,如果沒有則建立一個entry,如果有的話,判斷目前的key是否與要存入的key一緻,如果一緻,則替換值。如果key為空的話,則替換值。如果上面條件都不滿足,則建立一個entry對象。
ThreadLocal造成的記憶體洩漏
因為ThreadLocal是将自身作為弱引用存放在ThreadLocalMap中的,是以當一個ThreadLocal對象的強引用消失時。那麼這個key将會被回收,這時原來這個key對應的value值如果沒有被移除的話,那麼就永遠無法被通路到了。而且因為這個value值是作為entry節點的value引用指向的,value引用是一個強引用,那麼這時,這個value屬性就永遠無法被回收,也無法被通路,就會造成記憶體洩漏。
使用ThreadLocal如何避免記憶體洩漏?
使用ThreadLocal set了一個值以後,在這個線程結束之前,一定要調用remove方法移除存放的值。