発生要因
ThreadLocalMap のキーは弱参照であり、値は強参照である。
あるスレッドが長期間実行され続ける場合、値が特定のオブジェクトを強く参照していると、そのオブジェクトはガベージコレクションの対象にならず、メモリリークを引き起こす可能性がある。
解決策
簡単で、ThreadLocal を使用し終えたら即座に remove() メソッドを呼び出してメモリを解放することが望ましい。
なぜキーは弱参照に設計されているのか?
弱参照の利点は、メモリ不足が発生した際に JVM が即座に弱参照オブジェクトを回収できることだ。
たとえば:
WeakReference key = new WeakReference(new ThreadLocal());
この場合、key は弱参照であり、new WeakReference(new ThreadLocal()) は弱参照オブジェクトとなる。JVM がガベージコレクションを実行する際、弱参照オブジェクトを検出するとそれを回収する。
キーが回収されると、ThreadLocalMap は set や get 操作時にキーが null であるエントリをクリーンアップする。
深い理解
これは ThreadLocal の内部動作と JVM のガベージコレクション挙動に関する典型的かつ誤解されやすい問題である。以下にそれぞれのポイントを順に説明する:
一、正しい理解
はい、ThreadLocalMap の キーは弱参照(WeakReference<ThreadLocal>)であり、これは強参照によるメモリリークを防ぐためである。
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
}
この定義により以下のことが成り立つ:
- 特定の
ThreadLocalインスタンスに外部からの強参照が存在しない場合、その弱参照キーは GC によって回収される。 - キーが GC された場合、対応する
Entryは キーが null で値が残っている状態になる。
二、問題の核心:値は強参照であり、GC によって自動的に解放されない
キーが弱参照で GC されても、値は通常の強参照として扱われる。
つまり:
- キー(すなわち ThreadLocal インスタンス)が GC されても、値は依然として強参照として存在する。
ThreadLocal.remove()を呼び出さなければ、値に対応するオブジェクト(例えば大きなオブジェクトやスレッドに紐づいたリソース)はスレッドのThreadLocalMap内にずっと残る。
このメモリ領域は解放されず、スレッドが生きている限り保持される。
三、ThreadLocal が OOM を引き起こす理由
原因はスレッドの寿命と値の強参照にある:
- スレッドは長期にわたって生き続ける(例:スレッドプールのスレッド)
- ThreadLocal のキーが GC されたが、値は手動で削除されていない
- 値が大きなオブジェクトやリソース(DBコネクション、バッファ、マップなど)を参照している
- JVM は値を自動的に解放できないため、メモリリークが発生し、最終的に OOM に至る
四、実際の事例
ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
threadLocal.set(new byte[1024 * 1024 * 10]); // 10MB
// remove を呼び出さず、threadLocal への外部参照もなし
threadLocal = null;
threadLocal を null に設定しても(キーは GC される)、値(10MBの配列)はまだ ThreadLocalMap に残り、回収されない。
五、OOMリスクを回避する方法
- 手動で
ThreadLocal.remove()を呼び出す。ThreadLocal の使用後に必ずクリアする:
try {
threadLocal.set(value);
// 処理...
} finally {
threadLocal.remove();
}
- アリババのJava開発ガイドラインに従い、remove は必須である。
- スレッドプールのスレッドにおいて ThreadLocal を使用する際は、各タスク終了後に必ずクリアすることを確認する。
六、まとめ
| 要素 | 参照タイプ | GC後の挙動 |
|---|---|---|
| キー (ThreadLocal) | 弱参照 | 外部からの強参照がなければGCされる |
| 値 | 強参照 | 自動的にはGCされず、手動でのremoveが必要 |
キーがGCされても値は残り続け、スレッドが終了するまで解放されないため、メモリリークを引き起こし、最終的に OOM になる可能性がある。
ThreadLocalMap の expungeStaleEntries() メカニズムについてさらに掘り下げて知りたいですか?