ThreadLocalによるメモリリーク

ご列席の皆様、私は脚で自分自身を撃つための注目すべき方法を共有したいと思います。 しかし、ThreadLocalのような単純なことは、数ギガバイトのサーバーメモリを予期せず飲み込んでしまいました。



もちろん、サーバーのメモリはガベージを保存するよりも優れています。 だから私の間違いを繰り返さないでください。 つまり、このThreadLocalへのリンクや、このThreadLocalを最終的に参照するオブジェクトのグラフへのリンクをThreadLocalに保存しないでください。



image








最初に、コードを示します。



class X { ThreadLocal<Anchor> local = new ThreadLocal<Anchor>(); class Anchor { byte[] data = new byte[1024 * 1024]; } public Anchor getOrCreate() { Anchor res = local.get(); if (res == null) { res = new Anchor(); local.set(res); } return res; } public static void doLeakOneMoreInstance() { new X().getOrCreate(); } public static void main(String[] args) throws Exception { while (true) { doLeakOneMoreInstance(); System.out.println(Runtime.getRuntime().freeMemory() / 1024 / 1024 + " MB of heap left"); } } }
      
      







doLeakOneMoreInstanceを呼び出すたびにXの新しいインスタンスが作成され、値をThreadLocalに設定するメソッドが呼び出されます。その後、Xへの参照は完全に失われます。 コンストラクターで作成されたThreadLocalインスタンスへのリンクは、Xを超えることはありません。 この後、作成されたオブジェクトのグラフ全体への外部リンクは存在せず、存在することはできず、GCによって安全に削除できるようです。



しかし、そこにありました。 JVMが落ちると、「java.lang.OutOfMemoryError:Java heap space」というメッセージのみが残り、スタックトレースを冠します(ただし、指定されたクラスは数ギガバイトと非常に貪欲です)ため、このコードを若干のヒープサイズ制限で実行する価値があります。数ミリ秒で十分です)。



さらに読み進める前に、セルフテストとして質問に答えてみてください。上記のフラグメントにキーワードを1つだけ追加して、OOMを取り除く方法を教えてください。



もちろん、このような合成の例では、ThreadLocalがすべてを非難することは簡単に推測できます(ただし、それ以外に特別なものはないため)。ただし、Xが何百万もあり、死んで死んでいる大規模なプロジェクトでこれが発生した場合、問題は特定されませんシンプル。 誰かにとって解決策は明らかかもしれませんが、個人的には私の人生の1時間以上かかりました。



実際、問題は何ですか?



(以下で説明するものはすべて、OracleのJVM実装に当てはまります。ただし、他のものも影響を受ける可能性があります。)



この質問に答えるには、ThreadLocalの腸を少し掘り下げる必要があります。 事実、ThreadLocal変数のデータは変数に保存されるのではなく、直接Threadオブジェクトに保存されます。 各スレッドには、「弱い」キー(WeakHashMapの類似物)を持つ辞書の独自のインスタンスがあり、ThreadLocalインスタンスはキーとして機能します。 ThreadLocal変数に値を与えるように要求すると、実際に現在のスレッドを受け取り、そこから辞書を抽出し、辞書から値を受け取ります。自分自身をキーとして使用します。



ThreadLocalへのリンクがない場合、辞書でキーとして使用されるリンクは安全に無効化され、新しい要素が挿入されると、リモートGCオブジェクトを参照するエントリがクリーンアップされます。



問題はこのメカニズムにあります。キーへの弱いリンクはストリーム内の辞書に保存されますが、値への直接リンクは保存されます! どういうわけか、ThreadLocal値(この例ではAnchor型のオブジェクト)内から、それを含むThreadLocalが実現可能であることがわかります(この例では、Anchorは非静的クラスであるため、X型のオブジェクトへの暗黙的な参照があり、これがThreadLocalを参照しています) )、GCはThreadLocalを適切に削除できず、数世紀の終わりまで、または所有者のスレッドが生きている限り、自重を掛けたままになります。



さて、自己テストの質問に対する答えは非常に簡単です。 メモリリークが発生しないように、アンカークラスにstaticキーワードを追加して、リンクの悪循環を開くだけで十分です。



説明したThreadLocalの機能からもう1つの問題が発生することを言わなければなりません:値が属するスレッドが生きている限り、ThreadLocalへのリンクが失われても、誰もそれに関連するThreadLocal値の削除を保証しません:ポイント古い値は、ThreadLocalがこのスレッドに関連付けられた値にアクセスする場合にのみクリアされます。スレッドがネットワークI / Oを待機している場合、スリープまたはその他の時間のかかる操作を実行している場合、待機は無期限に遅延できます。



ThreadLocal、同僚に注意してください! ThreadLocalへのリンクをそれらに入れたり、データペタバイトを格納したりしないでください。 ThreadLocalの正しい使用を監視するよりも、Map <Thread、Value>を使用する方が簡単で信頼性が高い場合があります。この場合、少なくともオブジェクトのライフサイクルを制御します。



PSはい、そして私は「ThreadLocalでのメモリリーク」ではなく「ThreadLocalでのメモリリーク」という記事を意図的に呼び出しました。私の意見では、エラーはこのツールを使用するアプローチにあります。標準ライブラリ自体は完全に機能します。



All Articles