本文結(jié)合,ThreadLocal
內(nèi)存泄漏 和 DirectByteBuffer
釋放 講解 Java 中的 Reference
班利。
四種引用類型
- 強(qiáng)引用(Strong Reference):被強(qiáng)引用的對象饥漫,GC不能夠收集。常見得強(qiáng)引用對象得方式有: 賦值
Object obj = new Object()
罗标,集合引用list.add(new Object())
等等趾浅。 - 軟引用(Soft Reference):被軟引用的對象愕提,GC會在即將發(fā)生內(nèi)存溢出時,只要沒有對它強(qiáng)引用皿哨,就把它納入GC收集對象內(nèi)浅侨,進(jìn)行回收,軟引用通過
SoftReference
來實(shí)現(xiàn)证膨,它有兩個構(gòu)造(T reference)
和(T reference,ReferenceQueue<? super T> queue)
如输,和一個get()
方法,用于獲取引用的對象央勒。 - 弱引用(Weak Reference):被弱引用得對象不见,下一次GC時,只要沒有對它強(qiáng)引用就會納入GC收集對象內(nèi)崔步,進(jìn)行回收稳吮。與
SoftReference
相同,有兩個構(gòu)造和一個獲取引用對象得方法井濒。 - 虛引用(Phantom Reference):被虛引用得對象隨時可以被GC灶似,并且它不能通過
get()
獲取到引用對象,這個方法固定返回為null
瑞你,存在得意義在于酪惭,可以在對象被收集時,‘得到通知’ 進(jìn)而做一些其他工作者甲,例如记焊,DirectByteBuffer
就是利用PhantomReference
做直接內(nèi)存得釋放工作得冕末。
Reference
強(qiáng)引用(Strong Reference)底層實(shí)現(xiàn)無法感知,其他三種(Soft/Weak/Phantom Reference)均繼承于 abstract class Reference<T>
,他們的兩個 構(gòu)造方法 和 一個 獲取引用對象 的方法也 均來自于 Reference
翰意。
// SoftReference 簡單實(shí)現(xiàn)如下
public class SoftReference<T> extends Reference<T> {
public SoftReference(T referent) {
//在構(gòu)造 Soft/Weak/Phantom Reference 時堕汞,一般都需要掉用父級得回調(diào)歇盼。
super(referent);
this.timestamp = clock;
}
public T get() {
T o = super.get();
if (o != null && this.timestamp != clock)
this.timestamp = clock;
return o;
}
}
Refernece 狀態(tài)
- Active:最初狀態(tài)违寿,被 GC 特殊處理,當(dāng)引用可達(dá)性發(fā)生變化時扫倡,狀態(tài)會變?yōu)?
Pending
或者Inactive
谦秧,具體是那個狀態(tài),依據(jù)于這個Referece
在創(chuàng)建的時候是否撵溃,綁定了一個ReferenceQueue
疚鲤。 - Pending:在這個狀態(tài)時,
Reference
內(nèi)部變量Reference<Object> pending
會被賦值為當(dāng)前的引用(這個賦值操作是 JVM 負(fù)責(zé)的) 缘挑,由內(nèi)部啟動得線程掉用它得enqueu
方法進(jìn)入另一個狀態(tài)集歇。 - Enqueue:將
Reference<Object> pending
放入到ReferenceQueue
內(nèi)并喚醒,所有在這個隊(duì)列上等待得線程语淘, - Inactive:終態(tài)诲宇,到此為止际歼,這個
Reference
再也不能更改其他狀態(tài)了。
GC“回調(diào)/通知”業(yè)務(wù)線程
Reference
存在一個靜態(tài)代碼塊會啟動一個線程姑蓝,私有靜態(tài)成員 pending
由 垃圾收集器設(shè)置鹅心,并喚醒這個線程處理相關(guān)邏輯。摘要原代碼:
public abstract class Reference<T> {
//由 collector 設(shè)置纺荧,并喚醒下面啟動的線程
private static Reference<Object> pending = null;
static {
...
//處理邏輯旭愧,如果 pending == null 則 wait
//否則為clearner對象,則直接調(diào)用clear() clear方法一般會開啟線程宙暇,不應(yīng)該阻塞這個loop
//否則存在ReferenceQueue输枯,則放入 queue 中并喚醒等待的用戶線程
Thread handler = new ReferenceHandler(tg, "Reference Handler");
//最高優(yōu)先級
handler.setPriority(Thread.MAX_PRIORITY);
//守護(hù)線程
handler.setDaemon(true);
handler.start();
...
}
}
ThreadLocal & WeakReference 的使用
ThreadLocal
本質(zhì)上是一個門面類。通過它設(shè)置value占贫,本質(zhì)上是桃熄,在 Thread
的成員變量 ThreadLocal.ThreadLocalMap threadLocals = null;
中放入 Entry<k,v>
,其中 k 為這個 ThreadLocal
對象型奥,value 為需要存的數(shù)值瞳收。這樣的話就會有內(nèi)存泄漏的風(fēng)險,代碼描述如下:
//產(chǎn)生一個threadLocal對象
ThreadLocal<Object> threadLocal = new ThreadLocal();
...
//產(chǎn)生一個Thread t
new Thread(()->{
//某些場景下設(shè)置 ThreadLocal 變量
//本質(zhì)是在 threadLocalMap 中放入一個 Entry<threadLocal,Object>
threadLocal.set(new Object());
while (true) {
//之后去使用這個內(nèi)容
threadLocal.get();
}
}).start();
...
//在之后的某塊代碼桩引,將這個ThreadLocal給設(shè)置成null了
//之后,線程內(nèi)通過這個 ThreadLocal 其實(shí)已經(jīng)無法訪問到期望的 value 了
//但實(shí)際上收夸,Entry<threadLocal,Object> 仍然被 threadLocalMap 強(qiáng)引用坑匠,占用著內(nèi)存
threadLocal = null;
從開發(fā)角度來說,將 threadLocal = null;
=> threadLocal.remove()
就可以解決這個問題卧惜。從Java語言層面其實(shí)ThreadLocal
機(jī)制也存在其他操作來減少內(nèi)存溢出的風(fēng)險厘灼。
看一下ThreadLocalMap的實(shí)現(xiàn)
static class ThreadLocalMap {
//繼承了 WeakRefernce ,Entry的key其實(shí)時一個弱飲用
//也就是說咽瓷,當(dāng)ThreadLocal沒有任何強(qiáng)引用的時候设凹,通過 Reference#get()方法獲取key就會是 null
static class Entry extends WeakReference<ThreadLocal<?>> {
Entry(ThreadLocal<?> k, Object v) {
//k == ThreadLocal
//Entry 的 Key 時 WeakReference,當(dāng)沒有強(qiáng)引用時茅姜,會get到null
super(k);
value = v;
}
}
//這個方法內(nèi)闪朱,會移除掉 key == null 的 Entry
//這個方法會在,put 的時候钻洒,如果 key 為 null 時調(diào)用
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
}
}
簡單來說就是奋姿,ThreadLocalMap 中 Entry<K,V> K是一個 ThreadLocal 的弱引用,當(dāng) ThreadLocal 沒有任何強(qiáng)引用時素标,Entry的再獲取 K的時候称诗,會得到一個 null,再下一次 put 的時候头遭,就會從 ThreadLocalMap 中溢出掉所有 key 為 null 的 Entry寓免。
DirectByteBuffer & PhantomReference 的使用
//設(shè)置堆最大最小10m癣诱,直接內(nèi)存最大使用10m
//-Xmx10m -Xms10m -XX:MaxDirectMemorySize=10m
public static void main(String[] args) throws IOException {
//分配10m directbuffer
ByteBuffer buff1 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
//GC之后,在分配10m袜香,這里并不會內(nèi)存溢出
buff1 = null;
//這個顯示調(diào)用去掉撕予,其實(shí)在直接內(nèi)存不足的時候,也會自動出發(fā) FullGC
System.gc();
ByteBuffer buff2 = ByteBuffer.allocateDirect(1024 * 1024 * 10);
}
看上面的代碼舉例困鸥,很奇怪的一點(diǎn)是嗅蔬,GC 一般來說只會對 Java堆 以及 MateSpace(1.8 方法區(qū)實(shí)現(xiàn))做回收,那為什么直接內(nèi)存在運(yùn)行了 System.gc()
之后也仿佛被回收了呢疾就?
DirectByteBuffer 時怎么被釋放的呢澜术?
答案在于 DirectByteBuffer
的創(chuàng)建過程,代碼如下:
DirectByteBuffer(int cap) { // package-private
...
//關(guān)鍵在于這個 Cleaner猬腰,對于 DirectByteBuffer 的虛引用鸟废,并且接受一個 Runnable,這里是 Deallocator姑荷。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
這個Cleaner其實(shí)本質(zhì)上是一個對 DirectByteBuffer
對象的虛引用盒延,并且還接受了一個 Deallocator
對象(本質(zhì)上時一個Runnable,核心代碼是通過unsafe釋放內(nèi)存)鼠冕。上面講過添寺,再 GC 時,收集器一旦發(fā)現(xiàn)一個引用可達(dá)發(fā)生了變化懈费,就會走 GC“回調(diào)/通知”業(yè)務(wù)線程 這一套邏輯(上面講過了)计露。從而調(diào)用了 Cleaner#clean
,在這個例子中憎乙,這個方法票罐,其實(shí)就是運(yùn)行 Dealocator
,最終通過 unsafe.freeMemory(address)
釋放內(nèi)存泞边。
總的來說就是该押,通過Reference
(具體來說是PhantomPeference
)的通知/回調(diào)機(jī)制,在回收引用對象時阵谚,運(yùn)行一段用戶代碼蚕礼,調(diào)用unsafe.freeMemory(address)
釋放了直接內(nèi)存。
除了 ThreadLocal
DirectByteBuffer
外梢什,其他利用 Reference
在垃圾回收時闻牡,觸發(fā)一些用戶操作的類,還有很多绳矩。如:WeakHashMap
利用 WeakReference
防止內(nèi)存溢出罩润。
上述中,我這里將 Reference
這個機(jī)制翼馆,叫做 GC“回調(diào)/通知”業(yè)務(wù)線程 并不妥當(dāng)割以,原諒我已經(jīng)詞窮了金度,介于這個詞語可以直觀的反饋這個機(jī)制,還請大家見諒严沥。