ThreadLocal によって引き起こされるメモリ リークの分析

予備知識(参考)

オブジェクト o = 新しい Object();

これをオブジェクト参照と呼ぶことができ、メモリ内にオブジェクト インスタンスを作成する new Object() を呼び出すことができます。

o=nullと記述すると 、 o がヒープ内の object のオブジェクト インスタンスを指さなくなったことを意味するだけで、オブジェクト インスタンスが存在しないことを意味するわけではありません。

  • 強参照:  "Object obj=new Object()" など、プログラム コードで一般的な参照を指します。強参照が存在する限り、ガベージ コレクターは参照されたオブジェクト インスタンスをリサイクルしません。

  • ソフト参照: 有用ではあるが必須ではないいくつかのオブジェクトを説明するために使用されます。ソフト参照に関連付けられたオブジェクトの場合、システムでメモリ オーバーフロー例外が発生する前に、これらのオブジェクト インスタンスは 2 回目のリサイクルのリサイクル スコープに含まれます。このリサイクルに十分なメモリがない場合、メモリ オーバーフロー例外がスローされます。JDK 1.2 以降では、ソフト参照を実装するために SoftReference クラスが提供されています。

  • 弱い参照: 必須ではないオブジェクトの記述にも使用されますが、その強度はソフト参照よりも弱いため、弱い参照に関連付けられたオブジェクト インスタンスは、次のガベージ コレクションが発生するまでしか存続できません。ガベージ コレクターが動作すると、現在のメモリが十分かどうかに関係なく、弱参照のみに関連付けられたオブジェクト インスタンスがリサイクルされます。JDK 1.2 以降では、弱参照を実装するために WeakReference クラスが提供されています。

  • ファントム参照: ゴースト参照またはファントム参照とも呼ばれ、最も弱い参照関係です。オブジェクト インスタンスに仮想参照があるかどうかは、その生存時間にはまったく影響せず、仮想参照を通じてオブジェクト インスタンスを取得することはできません。オブジェクトの仮想参照関連付けを設定する唯一の目的は、オブジェクト インスタンスがコレクターによって再利用されたときにシステム通知を受け取ることです。その後、仮想参照を実装するためのクラスが提供されます。

メモリリーク現象

/**
 * 类说明:ThreadLocal造成的内存泄漏演示
 */
public class ThreadLocalOOM {
    private static final int TASK_LOOP_SIZE = 500;

    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5, 5,
            1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    static class LocalVariable {
        private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
    }

    final static ThreadLocal<LocalVariable> localVariable
            = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        /*5*5=25*/
        for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    //localVariable.set(new LocalVariable());
                    new LocalVariable();
                    System.out.println("use local varaible");
                    //localVariable.remove();
                }
            });

            Thread.sleep(100);
        }
        System.out.println("pool execute over");
    }

}

まず、各タスクで単純に配列を新規作成します。

 実際のメモリ使用量は約 25M に制御されていることがわかります。各タスクは新しい 5M 配列 (5*5=25M) を継続的に作成するため、これは非常に合理的です。

ThreadLocal を有効にすると

 

メモリ使用量は最大 150M まで上昇し、概ね 90M 程度で安定していますが、ThreadLocal を追加すると、メモリ使用量は実際にそれほど多くなるでしょうか?

そこで、次のコード行を追加します。

 再度実行してメモリの状況を確認します。

ピーク時のメモリ使用量も約 25M であることがわかります。これは、ThreadLocal を追加しなかった場合とまったく同じです。

これは、メモリ リークが実際に発生したことを完全に示しています。

分析する

ThreadLocal の以前の分析によると、各 Thread が ThreadLocalMap を維持していることがわかります。このマッピング テーブルのキーは ThreadLocal インスタンス自体であり、値は実際に保存する必要があるオブジェクトです。つまり、ThreadLocal 自体です。値は保存されず、スレッドが ThreadLocalMap から値を取得できるようにする A キーとしてのみ機能します。ThreadLocalMap を注意深く観察してください。このマップは、ThreadLocal の弱参照をキーとして使用します。弱参照オブジェクトは GC 中にリサイクルされます。

したがって、ThreadLocal を使用した後の参照チェーンは図のようになります。

図の破線は弱い参照を表します。

このように、threadlocal 変数が null に設定されている場合、threadlocal インスタンスへの強い参照は存在しないため、threadlocal は gc によってリサイクルされます。このようにして、null キーを持つエントリが ThreadLocalMap に表示され、null キーを持つこれらのエントリの値にアクセスする方法はありません。現在のスレッドが終了し続ける場合、null キーを持つこれらのエントリの値は、強力な参照チェーン:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value ですが、この値にはアクセスされないため、メモリ リークが発生します。

現在のスレッドが終了した後にのみ、現在のスレッドはスタックに存在せず、強参照が切断され、現在のスレッドとマップの値はすべて GC によってリサイクルされます。最善の方法は、ThreadLocal 変数を使用する必要がなくなった後で、remove() メソッドを呼び出してデータをクリアすることです。

実際、ThreadLocal の実装を見ると、get() であっても set() であっても、ある時点で expungeStaleEntry メソッドが呼び出され、Entry 内の Key が null である Value をクリアしていることがわかりますが、これはnot timely、そして it is not each 毎回実行されるため、場合によっては依然としてメモリ リークが発生する可能性があります。expungeStaleEntry メソッドのみが、remove() メソッドで明示的に呼び出されます。

表面的には、メモリ リークの根本原因は弱い参照の使用であるように見えますが、別の疑問も考慮する価値があります。それは、なぜ強参照ではなく弱い参照を使用するのかということです。

以下では 2 つの状況について説明します。

キーは 強い参照を使用しています: ThreadLocal を参照しているオブジェクトはリサイクルされていますが、ThreadLocalMap はまだ ThreadLocal への強い参照を保持しています。手動で削除しないと、ThreadLocal オブジェクト インスタンスはリサイクルされず、エントリ メモリ リークが発生します。

キーは 弱い参照を使用します: 参照された ThreadLocal オブジェクトはリサイクルされます。ThreadLocalMap は ThreadLocal への弱い参照を保持しているため、手動で削除されない場合でも、ThreadLocal オブジェクト インスタンスはリサイクルされます。この値は、次回 ThreadLocalMap が set、get、または Remove を呼び出すときにリサイクルされる可能性があります。

2 つの状況を比較すると、ThreadLocalMap のライフサイクルは Thread と同じくらい長いため、対応するキーを手動で削除しないとメモリ リークが発生しますが、弱い参照を使用することで追加の保護層を提供できることがわかります。

したがって、ThreadLocal メモリ リークの根本的な原因は次のとおりです。 ThreadLocalMap のライフ サイクルは Thread と同じくらい長いため、対応するキーが手動で削除されない場合、弱い参照が原因ではなく、メモリ リークが発生します。

ThreadLocalMap のキーを弱参照として設定する必要があるのはなぜですか?

ThreadLocalMapのsetメソッドとgetメソッドでは、キーがnullかどうかを判定し、キーがnullの場合は値もnullに設定されます。
このようにして、remove メソッドの呼び出しを忘れた場合でも、次に get、set、remove メソッドのいずれかを呼び出したときに対応する値がクリアされるため、メモリ リークが回避されます (追加の保護層に相当しますが、今後これらのメソッドを呼び出さない場合でも、メモリ リークの危険性があるため、適時に削除することをお勧めします)。

要約する

JVM は、メモリ リークを避けるために、ThreadLocalMap のキーを弱い参照として設定します。

JVM は、remove、get、set メソッドを呼び出すときに弱い参照をリサイクルします。

ThreadLocal が null キーを持つ多くのエントリを保存し、remove、get、set メソッドを呼び出さなくなると、メモリ リークが発生します。

スレッド プールThreadLocalを使用する場合は注意してください 。この場合、スレッドは常に繰り返し実行され、値も蓄積されます。

ThreadLocal を誤って使用すると、スレッドの安全性が低下します

/**
 * 非安全的ThreadLocal 演示
 */
public class ThreadLocalUnsafe implements Runnable {

    public static ThreadLocal<Number> numberThreadLocal = new ThreadLocal<Number>();
    /**
     * 使用threadLocal的静态变量
     */
    public static Number number = new Number(0);

    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum() + 1);
        //将其存储到ThreadLocal中
        numberThreadLocal.set(number);
        //延时2ms
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //输出num值
        System.out.println("内存地址:"+numberThreadLocal.get() + "," + Thread.currentThread().getName() + "=" + numberThreadLocal.get().getNum());
    }


    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    /**
     * 一个私有的类 Number
     */
    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }
    }
}

 出力:

内存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-2=5
内存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-0=5
内存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-4=5
内存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-1=5
内存地址:com.test.thread.ThreadLocalUnsafe$Number@5658172e,Thread-3=5

各スレッドが 5 を出力するのはなぜですか? 彼らは Number のコピーを自分だけで保管していませんか? 他のスレッドがまだこの値を変更できるのはなぜですか? コードを注意深く調べた結果、数値オブジェクトは静的であることがわかりました。そのため、各 ThreadLoalMap に保存された参照は、実際には同じオブジェクトです。この場合、他のスレッドがこの参照が指すオブジェクト インスタンスを変更すると、実際には、すべてのスレッドが保持するオブジェクト参照が指す同じオブジェクト インスタンスに影響します。これが、上記のプログラムが同じ結果を出力する理由です: 5 つのスレッドが同じ Number オブジェクトへの参照を保存します。スレッドがスリープすると、他のスレッドが num 変数を変更し、変更されたオブジェクト Number のインスタンスは同じコピーであるため、最終的な出力は同じです。

上記のプログラムが正常に動作するには、数値の静的な変更を削除して、各 ThreadLoalMap が操作に異なる数値オブジェクトを使用できるようにする必要があります。

概要: ThreadLocal は、スレッドの安全性ではなく、スレッドの分離のみを保証します。

おすすめ

転載: blog.csdn.net/qq_45443475/article/details/131203710