Javaとメモリリーク
メモリリークは検出するのが難しい問題です。
プロファイラなどの助けを借りることも大事ですが、最大の防御線はプログラマ個々人の意識です。
Javaとメモリ管理
Javaは基本的にメモリを意識しなくてよい言語です。
どこからも参照されなくなったオブジェクトはGarbage Colllectorが自動で解放してくれます。
しかし全くメモリを意識しなくてよいわけではありません。
メモリリークの例
生成コストの高いオブジェクトをキャッシュして再利用することにしたとします。 しかしキャッシュには注意が必要です。
キャッシュ=オブジェクトへの参照をずっと保持する=ずっとGCされない、ということになります。 このような状態が意図せずに長期間継続した場合、メモリリークになります。
キャッシュを実装するときには、いつどのようにオブジェクトを解放するか注意が必要です。
ではプログラマは具体的になにを気をつければよいのでしょうか。
指針
全Javaプログラマの指針といえば「Effective Java」です。
項目7にメモリリークの詳細と具体例が記載されています。
この本は全体的に読みにくい本ですがこの項目に関してはよいサンプルコードが掲載されていて理解しやすいです。
プログラマとしてはここに記載されている内容を理解した上でコーディング、コードレビューに望みたいものです。
以下に骨子のみまとめます。
メモリリークの原因
- 使われなくなった参照が原因
- 参照が残っているとGCは使っていないことを判断できない
- 使われなくなった参照の廃棄方法には2種類ある
- 最も狭いスコープで利用してスコープの外に出す
- nullをセットする
- 基本的に1の方法を取る
- 2は例外的措置でやむを得ない場合にのみ実施する
(筆者感想) 1が徹底できれば楽ですがDIするためにオブジェクトをローカル変数で保持せず、フィールドで保持するケースは多いと思います。 DIが好きな人は2の可能性を常に意識する必要があります。
対策
- メモリリークの検出は難しい
- 試験で検出されることは稀で、Production環境でもすぐに顕在化するわけではない
- コードレビューやプロファイラに頼らざるを得ない
- 後工程で検出されることに期待するのではなく、プログラマ自身が気をつけて予防することが重要
メモリリーク注意すべきケース
1. 独自のメモリ管理をしている
- Stack、Queueなどを実装したとき
- 要素を解放してもnullをセットしない限り、GCの対象にならない
- 対策として要素が解放されるときは明示的にnullにセットする
2. オブジェクトのキャッシュ
- リークする理由は前述の通り
- キャッシュからオブジェクトを削除する方法には3つある
- WeakHashMapを使う(外部からのキーへの参照がなくなると自動削除される)
- バックグラウンド監視スレッドで一定時間で削除
- 追加時の副作用として古いものを消す
3. リスナー、コールバックの登録
- 登録しっぱなしで削除手続きがないとリークする
- 対策としてWeakHashMapを使う
Webフレームワークにおける開発の考察
リスクに直面する機会は少ない
一般的なWebフレームワークなどで開発している場合、意識する機会は少ないかもしれません。
リクエストやセッションごとに生成、破棄されるライフサイクルが短いオブジェクトはあまり神経質にならなくても大丈夫だと思います。 それらのオブジェクトの中で参照を解放することを忘れたとしても一定期間経過すればそれらオブジェクトは破棄され、そのオブジェクトが参照していたオブジェクトは参照を失いGCの対象になるからです。
Servletのように再利用される、長いライフサイクルを持つオブジェクトはフィールドに保持している参照に注意が必要です。 しかしスレッドセーフにするために可変フィールドをServletから排除することはよく知られたプラクティスであり、そもそもフィールド自体ないことも少なくないと思います。
しかし落とし穴はあります。
落とし穴1:ライフサイクルの長いオブジェクトのフィールド
例えばServletのフィールドにオブジェクトへの参照を持っていた場合、そのオブジェクトはGCされません。 フィールドが可変でもFinalでも関係ありません。
そのオブジェクトが更に別のオブジェクトを参照していて、そこに使われなくなった参照が蓄積されていたらメモリリークになります。
Webフレームワークでもライフサイクルの長いオブジェクトのフィールドには注意を払う必要があります。
落とし穴2:staticフィールドや定数
staticフィールドや定数がGC対象にならないことは直感的に理解できると思います。これらはGC rootとみなされ、GC rootが参照しているオブジェクトはGCの対象になりません。それだけでなく、そのオブジェクトが参照しているオブジェクトも同様にGC対象にならない点に注意が必要です。
staticフィールドや定数で参照を保持する場合、参照先のオブジェクトに使われなくなった参照が蓄積しないか注意が必要です。蓄積される場合は不要になった時点でnullをセットしましょう。
参照の循環は問題ない
参照が循環した場合、参照が永久に保持されてGC対象にならずメモリリークになると考えるかもしれません。しかしJavaでは大丈夫です。
GCにも色々な方式があり、Reference Countという方式では参照が循環するとメモリリークが発生します。
しかしJVMで採用されている方式でGC rootと呼ばれる参照の起点から辿れるかどうかでGC要否を判断します。GC rootから辿れなければ参照が循環していても使われなくなった参照と判断されてGC対象になります。
GC rootとはなにか?とは難しいトピックです。正確に理解するのは困難ですが大雑把に以下のように理解しておけばよいと思います。
- 実行中のスレッド(例: mainスレッド)のローカル変数
- 実行中のスレッドのThread Local
- staticフィールド
- 定数
結論としては、staticや定数でない限り実行中のスレッドで不要になったインスタンスはGC対象になるのでオブジェクト間の相互参照に注意する必要はありません。
結論
- 参照を蓄積する箇所があれば、参照がいつ削除されるか確認する
- staticや定数ではないのである処理が完了するとローカルスコープを抜けて参照が削除される
- ローカルスコープを抜けることはないが参照を明示的に削除する機構がある
- 参照が削除されるタイミングがなければ明示的にnullをセットする