web-dev-qa-db-ja.com

ThreadLocalおよびメモリリーク

複数の投稿で言及されています:ThreadLocalの不適切な使用はメモリリークを引き起こします。 ThreadLocalを使用してメモリリークがどのように発生するかを理解するのに苦労しています。

私がそれを以下のように理解した唯一のシナリオ:

Webサーバーは、スレッドのプールを保持します(たとえば、サーブレット用)。スレッドは死なないため、ThreadLocalの変数が削除されない場合、これらのスレッドはメモリリークを引き起こす可能性があります。

このシナリオでは、「Perm Space」メモリリークについては言及していません。それはメモリリークの唯一の(主要な)ユースケースですか?

47
Sandeep Jindal

PermGenの枯渇ThreadLocalとの組み合わせは、多くの場合classloader leaksによって引き起こされます。

例:
worker threadsのプールを持つアプリケーションサーバーを想像してください。
アプリケーションサーバーが終了するまで、それらは保持されます。
デプロイされたWebアプリケーションは、クラスの1つでstaticThreadLocalを使用して、スレッドローカルデータ、別のクラスのインスタンス(let WebアプリケーションのSomeClass)と呼びます。これはワーカースレッド内で行われます(たとえば、このアクションはHTTPリクエストから発生します)。

重要:
定義によるThreadLocalvalueへの参照は、「所有」スレッドがなくなるか、ThreadLocal自体がなくなるまで保持されます。到達可能。

WebアプリケーションがシャットダウンThreadLocalへの参照のクリアに失敗すると、次のような悪いことが起こります。
ワーカースレッドは通常死ぬことはなく、ThreadLocalへの参照は静的であるため、ThreadLocalの値stillリファレンスのインスタンスSomeClass、Webアプリケーションのクラス-Webアプリケーションが停止していても!

その結果、Webアプリケーションのclassloaderはガベージコレクションできません。つまり、すべてのクラス(およびすべての静的データ) Webアプリケーションはロードされたままです(これはPermGenメモリプールとヒープに影響します)。
Webアプリケーションの再デプロイメントを繰り返すたびに、permgen(およびヒープ)の使用量が増加します。

=>これはpermgenリークです

この種のリークの一般的な例の1つは、log4jの このバグ (その間に修正済み)です。

68
MRalwasser

この質問に対する受け入れられた回答と、この問題に関するTomcatからの「重大な」ログは誤解を招きます。そこにあるキークォートは:

定義上、ThreadLocal値への参照は、「所有」スレッドが消滅するまで、またはThreadLocal自体に到達できなくなるまで保持されます。[私の強調]。

この場合、ThreadLocalへの参照は、GCのターゲットになったクラスの静的な最終フィールドと、ワーカースレッドからの参照のみです。ただし、ワーカースレッドからThreadLocalへの参照は WeakReferences !です。

ただし、ThreadLocalの値は弱参照ではありません。そのため、ThreadLocalのvaluesにアプリケーションクラスへの参照がある場合、これらはClassLoaderへの参照を維持し、GCを防ぎます。ただし、ThreadLocalの値が単なる整数または文字列、またはその他の基本的なオブジェクトタイプ(上記の標準コレクションなど)である場合、問題はないはずです(ブート/システムクラスローダーのGCを防ぐだけです)とにかく起こることはありません)。

ThreadLocalを終了したら、明示的にThreadLocalをクリーンアップすることをお勧めしますが、 引用したlog4jバグ の場合、空は絶対に落ちていません(レポートからわかるように、値は空のハッシュテーブルです)。

デモ用のコードを次に示します。最初に、ファイナライズ時にSystem.outに出力する親を持たない基本的なカスタムクラスローダーの実装を作成します。

import Java.net.*;

public class CustomClassLoader extends URLClassLoader {

    public CustomClassLoader(URL... urls) {
        super(urls, null);
    }

    @Override
    protected void finalize() {
        System.out.println("*** CustomClassLoader finalized!");
    }
}

次に、このクラスローダーの新しいインスタンスを作成するドライバーアプリケーションを定義し、それを使用してThreadLocalでクラスをロードし、クラスローダーへの参照を削除してGCを実行できるようにします。まず、ThreadLocal値がカスタムクラスローダーによってロードされたクラスへの参照である場合:

import Java.net.*;

public class Main {

    public static void main(String...args) throws Exception {
        loadFoo();
        while (true) { 
            System.gc();
            Thread.sleep(1000);
        }
    }

    private static void loadFoo() throws Exception {
        CustomClassLoader cl = new CustomClassLoader(new URL("file:/tmp/"));
        Class<?> clazz = cl.loadClass("Main$Foo");
        clazz.newInstance();
        cl = null;
    }


    public static class Foo {
        private static final ThreadLocal<Foo> tl = new ThreadLocal<Foo>();

        public Foo() {
            tl.set(this);
            System.out.println("ClassLoader: " + this.getClass().getClassLoader());
        }
    }
}

これを実行すると、CustomClassLoaderが実際にガベージコレクションされていないことがわかります(メインスレッドのローカルスレッドには、カスタムクラスローダーによってロードされたFooインスタンスへの参照があるため)。

 $ Java Main 
 ClassLoader:CustomClassLoader @ 7a6d084b 

ただし、Fooインスタンスではなく単純な整数への参照を含むようにThreadLocalを変更すると、次のようになります。

public static class Foo {
    private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>();

    public Foo() {
        tl.set(42);
        System.out.println("ClassLoader: " + this.getClass().getClassLoader());
    }
}

次に、カスタムクラスローダーisがガベージコレクションされていることがわかります(メインスレッドのローカルスレッドはシステムクラスローダーによってロードされた整数への参照のみを持つため) ):

 $ Java Main 
 ClassLoader:CustomClassLoader @ e76cbf7 
 *** CustomClassLoader finalized!

(Hashtableでも同様です)。そのため、log4jの場合、メモリリークやバグはありませんでした。彼らはすでにハッシュテーブルをクリアしており、これはクラスローダーのGCを保証するのに十分でした。 IMO、Tomcatにバグがあり、アプリケーションクラスへの強力な参照を保持しているかどうかに関係なく、明示的に.remove()dされていないすべてのThreadLocalsのシャットダウン時にこれらの「重大」エラーを無差別に記録します。少なくとも一部の開発者は、ずさんなTomcatログのsay-soの幻のメモリリークを「修正」するために時間と労力を費やしているようです。

29
Neil Madden

スレッドローカルには本質的に問題はありません。メモリリークは発生しません。彼らは遅くありません。それらは、非スレッドローカルの対応物よりもローカルです(つまり、情報隠蔽プロパティが優れています)。もちろん誤用される可能性がありますが、他のほとんどのプログラミングツールも同様です。

これを参照してください link by Joshua Bloch

1
MayurB

ThreadLocalが常に存在する場合、メモリリークが発生します。 ThreadLocalオブジェクトがGCである可能性がある場合、メモリリークは発生しません。 ThreadLocalMapのエントリはWeakReferenceを拡張するため、ThreadLocalオブジェクトがGCになった後、エントリはGCになります。

以下のコードは多くのThreadLocalを作成しますが、メモリリークは発生せず、mainのスレッドは常に有効です。

// -XX:+PrintGCDetails -Xms100m -Xmx100m 
public class Test {

    public static long total = 1000000000;
    public static void main(String[] args) {
        for(long i = 0; i < total; i++) {
            // give GC some time
            if(i % 10000 == 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            ThreadLocal<Element> tl = new ThreadLocal<>();
            tl.set(new Element(i));
        }
    }
}

class Element {
    private long v;
    public Element(long v) {
        this.v = v;
    }
    public void finalize() {
        System.out.println(v);
    }
}
0
MaJinliang

メモリリークの問題がないThreadLocalの代替手段は次のとおりです:

class BetterThreadLocal<A> {
  Map<Thread, A> map = Collections.synchronizedMap(new WeakHashMap());

  A get() {
    ret map.get(Thread.currentThread());
  }

  void set(A a) {
    if (a == null)
      map.remove(Thread.currentThread());
    else
      map.put(Thread.currentThread(), a);
  }
}

注:新しいメモリリークのシナリオがありますが、非常にまれであり、簡単なガイドラインに従うことで回避できます。シナリオは、BetterThreadLocal内のThreadオブジェクトへの強い参照を保持しています。

とにかくスレッドへの強い参照を保持することはありません。なぜなら、作業が完了したときに常にスレッドをGCできるようにしたいからです...メモリリークのないThreadLocalです。

誰かがこれをベンチマークする必要があります。私はそれがJavaのThreadLocalとほぼ同じくらい速いと期待しています(両方とも基本的に弱いハッシュマップルックアップを行い、1つだけがスレッドを検索し、もう1つはThreadLocalを検索します)。

JavaXのサンプルプログラム

最後の注意:私のシステム( JavaX )もすべてのWeakHashMapを追跡し、定期的にそれらをクリーンアップするため、最後の非常にまれな穴が塞がれます(長命のWeakHashMapはクエリされませんが、まだ古いエントリがあります)。

0
Stefan Reich

前の投稿では問題を説明していますが、解決策は提供していません。 ThreadLocalを「クリア」する方法がないことがわかりました。リクエストを処理しているコンテナ環境では、すべてのリクエストの最後に.remove()を呼び出しました。コンテナ管理のトランザクションを使用すると問題が発生する可能性があることを認識しています。

0
Steve11235

コードの下では、反復のインスタンスtをGCにすることはできません。これはThreadLocal & Memory Leakの例かもしれません

public class MemoryLeak {

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100000; i++) {
                    TestClass t = new TestClass(i);
                    t.printId();
                    t = null;
                }
            }
        }).start();
    }


    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}
0
Yanhui Zhou