虛引用(WeakReference)
在開始之前,需要區分 Java 引用中的強、軟、弱、虛引用,ThreadLocal 使用了弱引用,它是問題的關鍵。
只有弱引用指向的對象,只要進行 GC 時便會被清除。
ThreadLocal 內存模型
每個線程自己保存一個 Map,即 ThreadLocalMap,這個 Map 以 ThreadLocal 對象爲鍵,且 ThreadLocalMap 的 Entry 繼承了 WeakReference 類,是一個對 ThreadLocal 對象的弱引用,源碼如下(ThreadLocal.ThreadLocalMap.Entry)。
使用 ThreadLocal 時容易產生一種感覺,即我們保存的數據似乎保存在 ThreadLocal 對象上,實際上它們保存在當前的 Thread 對象上。ThreadLocal 只是提供了一個操作的框架。
ThreadLocal 內存泄漏原理
如果我們在線程中使用 ThreadLocal 不當,將會導致保存的數據無法被回收,過程如下:
ThreadLocal 內存泄漏現象觀測
清楚了原理以及解決辦法,我們通過一個程序來驗證上述過程,並觀測內存泄漏現象。
運行環境
OpenJDK13、G1 回收器、堆空間 20M
命令行參數:-Xmx20M -Xlog:gc*
測試代碼
import java.util.ArrayList;
/** ThreadLocal 內存泄漏觀測 */
public class Main {
static class ValueObject {
private long[] data = new long[131072]; // 需要 1M 空間 (1024 * 1024 / 8)
}
public static void main(String[] args) throws InterruptedException {
int threadNumber = 10;
while (threadNumber-- > 0) {
Thread worker = new Thread(() -> {
int localCount = 15;
var locals = new ArrayList<>(localCount);
while (localCount-- > 0) {
ThreadLocal<ValueObject> newLocal = new ThreadLocal<>();
newLocal.set(new ValueObject());
locals.add(newLocal);
// newLocal.remove();
}
locals = null;
System.gc();
}, "工作線程");
worker.start();
worker.join();
}
System.out.println("運行結束");
}
}
現象
輸出的日誌中,可以看到程序報 OutOfMemoryError,且即使垃圾回收器 Full GC 也無法釋放內存。而將上文代碼中 newLocal.remove 一行註釋解開,則不會發生該錯誤,對比如下,可以發現二者對堆空間的使用上存在很大差距:
總結
解決這個問題,需要在用完 ThreadLocal 後調用 remove()。
另外通過實驗發現,這個現象不是必然發生,分析 GC 日誌可以發現,很多時候,垃圾回收器仍然可以成功回收一很大部分空間。