ThreadLocal 內存模型、內存泄漏原因、現象觀測、解決

虛引用(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 日誌可以發現,很多時候,垃圾回收器仍然可以成功回收一很大部分空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章