前言
平時并發程式設計,除了維護修改共享變量的場景,有時我們也需要為每一個線程設定一個私有的變量,進行線程隔離,java提供的ThreadLocal可以幫助我們實作,而講到ThreadLocal則不得不講講java的四種引用,不同的引用類型在GC時表現是不一樣的,引用類型Reference有助于我們了解如何快速回收某些對象的記憶體或對執行個體的GC控制
- 四種引用類型在JVM的生命周期
- 引用隊列(ReferenceQueue)
- ThreadLocal的實作原理和使用
- FinalReference和finalize方法的實作原理
- Cheaner機制
關注公衆号,一起交流,微信搜一搜: 潛行前行
1 四種引用類型在JVM的生命周期
強引用(StrongReference)
- 建立一個對象并賦給一個引用變量,強引用有引用變量指向時,永遠也不會垃圾回收,JVM甯願抛出OutOfMemory異常也不會回收該對象;強引用對象的建立,如
Integer index = new Integer(1);
String name = "csc";
- 如果中斷所有引用變量和強引用對象的聯系(将引用變量指派為null),JVM則會在合适的時間就會回收該對象
軟引用(SoftReference)
- 和強用引用不同點在于記憶體不足時,該類型引用對象會被垃圾處理器回收
- 使用軟引用能防止記憶體洩露,增強程式的健壯性。SoftReference的特點是它的一個執行個體儲存對一個Java對象的軟引用,該軟引用的存在不妨礙垃圾收集線程對該Java對象的回收
- SoftReference類所提供的get()方法傳回Java對象的強引用。另外,一旦垃圾線程回收該對象之後,get()方法将傳回null
String name = "csc";
//軟引用的建立
SoftReference<String> softRef = new SoftReference<String>(name);
System.out.println(softRef.get());
弱引用(WeakReference)
- 特點:無論記憶體是否充足,隻要進行GC,都會被回收
String name = "csc";
//弱引用的建立
WeakReference<String> softRef = new WeakReference<String>(name);
System.out.println(softRef.get()); //輸出 csc
System.gc();
System.out.println(softRef.get()); //輸出 null
//弱引用Map
WeakHashMap<String, String> map = new WeakHashMap<String, String>();
虛引用(PhantomReference)
- 特點:如同虛設,和沒有引用沒什麼差別;虛引用和軟引用、弱引用不同,它并不決定對象的生命周期。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收
- 要注意的是,虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之關聯的引用隊列中。程式可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否将要被垃圾回收
public static void main(String[] args) {
String name = "csc";
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(name, queue);
//PhantomRefrence的get方法總是傳回null,是以無法通路對應的引用對象。
System.out.println(pr.get()); // null
System.gc();
System.out.println(queue.poll()); //擷取被垃圾回收的"xb"的引用ReferenceQueue
}
引用類型 | 被垃圾回收時間 | 場景 | 生存時間 |
---|---|---|---|
強引用 | 從來不會 | 對象的一般狀态 | JVM停止運作時終止 |
軟引用 | 當記憶體不足時 | 對象緩存 | 記憶體不足時終止 |
弱引用 | 正常垃圾回收時 | 對象緩存 | 垃圾回收後終止 |
虛引用 | 正常垃圾回收時 | 跟蹤對象的垃圾回收 | 垃圾回收後終止 |
2 引用隊列(ReferenceQueue)
- 引用隊列可以配合軟引用、弱引用及虛引用使用;當引用的對象将要被JVM回收時,會将其加入到引用隊列中
ReferenceQueue<String> queue = new ReferenceQueue<String>();
WeakReference<String> pr = new WeakReference<String>("wxj", queue);
System.gc();
System.out.println(queue.poll().get()); // 擷取即将被回收的字元串 wxj
3 ThreadLocal的原理和使用
ThreadLocal 的實作原理
- 每個線程都内置了一個ThreadLocalMap對象
public class Thread implements Runnable {
/* 目前線程對于的ThreadLocalMap執行個體,ThreadLocal<T>作為Key,
* T對應的對象作為value */
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocalMap作為ThreadLocal的内部類,實作了類似HashMap的功能,它元素Entry繼承于WeakReference,key值是ThreadLocal,value是引用變量。也就是說jvm發生GC時value對象則會被回收
public class ThreadLocal<T> {
//ThreadLocal對象對應的hash值,使用一個靜态AtomicInteger實作
private final int threadLocalHashCode = nextHashCode();
//設定value
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//擷取value
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//擷取目前線程的ThreadLocalMap,再使用對象ThreadLocal擷取對應的value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
....
//類似HashMap的類
static class ThreadLocalMap {
//使用開放位址法解決hash沖突
//如果hash出的index已經有值,通過算法在後面的若幹位置尋找空位
private Entry[] table;
...
//Entry 是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal不保證共享變量在多線程的安全性
- 從ThreadLocal的實作原理可知,ThreadLocal隻是為每個線程儲存一個副本變量,副本變量的修改不影響其他線程的變量值,是以ThreadLocal不能實作共享變量的安全性
ThreadLocal 使用場景
- 線程安全,包裹線程不安全的工具類,比如java.text.SimpleDateFormat類,當然jdk1.8已經給出了對應的線程安全的類java.time.format.DateTimeFormatter
- 線程隔離,比如資料庫連接配接管理、Session管理、mdc日志追蹤等。
ThreadLocal記憶體洩露和WeakReference
- ThreadLocalMap.Entry是弱引用,弱引用對象是不管有沒有被引用都會被垃圾回收
- 發生記憶體洩漏一般是線上程池的線程,生命周期長,threadLocals引用會一直存在,當其存放的ThreadLocal被回收(弱引用生命周期短)後,它對應的Entity成了e.get()==null的執行個體。線程不死則Entity一直不會被回收,這就發生了記憶體洩漏
- 如果線程跨業務操作相同的ThreadLocal,還會造成變量安全問題
- 通常在使用完ThreadLocal最好調用它的remove();在ThreadLocal的get、set的時候,最好檢查目前Entity的key是否為null,如果是null就把Entity釋放掉,value則會被垃圾回收
4 finalize方法的實作原理FinalReference
final class Finalizer extends FinalReference<Object> {
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
/* Invoked by VM */
static void register(Object finalizee) {
new Finalizer(finalizee);
}
private static class FinalizerThread extends Thread {
....
public void run() {
...
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
//這裡會實作Object.finalize的調用
f.runFinalizer(jla);
....
}
static {
...
Thread finalizer = new FinalizerThread(tg);
... //執行Object.finalize的守護線程
finalizer.setDaemon(true);
finalizer.start();
}
- cpu資源比較稀缺的情況下FinalizerThread線程有可能因為優先級比較低而延遲執行finalizer對象的finalize方法
- 因為finalizer對象的finalize方法遲遲沒有執行,有可能會導緻大部分finalizer對象進入到old分代,此時容易引發old分代的gc,甚至fullgc,gc暫停時間明顯變長
5 Cheaner機制
- 上一篇文章有介紹到jdk1.8的Cleaner架構篇:ByteBuffer和netty.ByteBuf詳解
歡迎指正文中錯誤
參考文章
- ThreadLocal原理及使用場景大揭秘
- JDK源碼分析之FinalReference完全解讀
- 一次 Young GC 的優化實踐
- Netty資源洩露檢測
- 避免使用Finalizer和Cleaner機制
- 【JAVA Reference】Cleaner 源碼剖析(三)