- 技術 💻
JavaのメモリリークとGC8選|OOM・ヒープダンプ
ガベージコレクション(GC)のある言語を使っていると、メモリのことを忘れられますよね。
でも本番では、GC言語なのにメモリリークし、ヒープが空いているのにOutOfMemoryが出ます。今回は、「GCがあるからメモリは気にしなくていい」への反例を8つ集めました。1つでも知らなかったら儲けものです。
① GC言語でも、メモリリークは起きます
GCが回収するのは、「どこからも参照されていない」オブジェクトだけです。
つまり、使い終わったのに参照が残っていれば、GCは「まだ使っている」と判断して回収しません。定番の犯人がこれです。
static Map<String, Data> cache = new HashMap<>();
// 入れるだけ入れて、消すコードがない
staticなコレクションに溜め続けるパターン。リスナーの解除忘れも同類です。
GCは「不要」を検知するのではなく、「参照ゼロ」を検知する。この一文が、GC言語のメモリリークの全てなんです。
② GCの裏で、世界は止まっています
GCが動く瞬間、アプリケーションの全スレッドが一時停止します。Stop the Worldと呼ばれる時間です。
現代のGCはこの停止を劇的に短くしました。JavaのZGCのように、停止を1ミリ秒未満に抑えることを目指すGCもあります。それでも、ゼロにはなっていません。
「レスポンスタイムのグラフに、数十ミリ秒〜の棘が周期的に立つ」「タイムアウトがたまに起きるが再現しない」——この症状の容疑者リストに、GC停止は常に入れておくべきなんですよね。
アプリのログに何も出ないのに遅い。そういうときは、アプリの下の階を見るんです。
③ ヒープが空いていても、OutOfMemoryは出ます
OutOfMemoryError=ヒープ不足、とは限りません。
メモリにはヒープ以外の領域があります。
- クラス情報を置くメタスペース
- スレッドごとのスタック
- ヒープ外に直接確保されるバッファ
それぞれが別々に枯渇し、それぞれのOOMを出します。たとえばスレッドを無限に生成すれば、ヒープがガラガラでも「新しいスレッドが作れない」というOOMで死にます。
エラーメッセージのどの領域が枯れたのかを読む。そこから調査が始まるんです。
④ 参照には、「強さ」があります
参照は全部同じではありません。強さの段階があります。
| 参照の種類 | GCとの関係 |
|---|---|
| 強参照(普段書いている参照) | 繋がっている限り、絶対に回収されない |
| ソフト参照(SoftReference) | メモリが苦しくなったら、回収してよい |
| 弱参照(WeakReference) | 他に強参照がなければ、次のGCで回収してよい |
①のようなキャッシュ用途に効くのが、この使い分けです。「あれば使うが、メモリが苦しいなら捨てていい」ならソフト参照。「キーの持ち主がいなくなったら一緒に消えてほしい」なら弱参照(WeakHashMapなど)。
「絶対に持つ」と「あれば嬉しい」を区別して書ける——知っていると設計の語彙が増える機能なんです。
⑤ finalizeに、後始末を任せてはいけません
「オブジェクトが回収されるときに呼ばれるメソッドがあるなら、そこでファイルを閉じればいい」——危険な発想です。
finalizeは、いつ呼ばれるか分からず、呼ばれない可能性すらあります。GCが走らなければ永遠に来ませんし、実行順序も保証されません。Javaでは非推奨になり、将来の削除が予告されています。
リソースの後始末は、try-with-resourcesのような「スコープで確実に閉じる」仕組みで書く。
「いつか誰かがやってくれる」は、メモリ管理では設計になりません。
⑥ メモリ使用量が減らない=リーク、ではありません
監視グラフでメモリが高止まりしている。リークでしょうか?
そうとは限りません。多くのランタイムは、一度OSから確保したメモリを、使い終わってもすぐには返しません。また借りるのは高くつくので、手元にキープするんです。
つまり「プロセスのメモリ使用量」は、いま使っている量ではなく、これまでの最大需要の名残だったりします。
リークかどうかは、使用量の絶対値ではなく、GC後のヒープ使用量が右肩上がりを続けるかで判断する。見るグラフが違うんです。
⑦ 「ほとんどのオブジェクトは、すぐ死ぬ」が設計の土台です
GCの内部には、美しい観察があります。
生成されたオブジェクトの大多数は、生まれてすぐ不要になる。一時変数、途中結果、リクエストの間だけ生きるデータ。長生きするのは、ごく一部です。
だから現代のGCは、メモリを「若い世代」と「古い世代」に分け、若い世代だけを高頻度・高速に掃除します。世代別GCです。
大量の短命オブジェクトはむしろ得意分野。逆に、中途半端に長生きするオブジェクトを量産する設計が、GCを一番苦しめるんです。
⑧ リーク調査の武器は、ヒープダンプです
最後は、実務の話です。
メモリリークの調査は、勘ではなくヒープダンプ(メモリの中身のスナップショット)で行います。見方の基本は2つ。
- ヒストグラム:どのクラスのインスタンスが何個・何MBあるかで、犯人の当たりを付ける
- 参照経路:そのオブジェクトを誰が参照しているかで、①のような「握っている場所」を特定する
時間を空けて2回取り、増え続けているものを探す。これだけで、リーク調査の8割は戦えるんです。
番外編:知っておくと得する小ネタ
System.gc() は、お願いにすぎません
「GCを実行せよ」ではなく「そろそろどうですか」という提案です。無視されることもあります。これに頼るコードは、だいたい設計を見直すサインです。
殺したのはアプリではなく、OSかもしれません
Linuxには、メモリが枯渇したときにOSがプロセスを強制終了する仕組み(OOM Killer)があります。アプリのログに何も残さず消えていたら、OS側のログを見るんですよね。
-Xmxは「上限」であって「予約」ではありません
ヒープの最大値の指定は、そこまで育ち得るという枠です。コンテナのメモリ制限との関係を間違えると、③やOOM Killerの餌食になります。
RJCでも、メモリと向き合っています
RJCが携わる金融や決済のシステムは、長時間稼働し続けることが前提の世界です。じわじわ育つリークは、まさに⑧の手順で追い詰める対象なんですよね。
社内のJava探求グループでも、こういう仕組みの話を掘り下げています。Techチームで共有しながら開発しています。
よくいただくご質問
Q. まずどれから押さえるべきですか?
A. ①かなと思います。「GCは参照ゼロしか回収しない」の一文だけで、リークの原因の大半が説明できるんですよね。
Q. GCのチューニングは必要ですか?
A. まず計測が先です。②のような停止時間が実害になっているかをGCログで確認してから。多くの場合、①のリーク退治と⑦を意識した設計の方が効きます。
Q. 監視では何を見ればいいですか?
A. ⑥のとおり「GC後のヒープ使用量の推移」です。これが右肩上がりならリーク、平らなら健康。1本のグラフで判断できます。
Q. 未経験からでも、分かるようになりますか?
A. なります。GCは「片付けロボットは、名札の付いた物を捨てられない」という比喩で全部説明できる世界なんです。
自動化されたのは「解放」だけです
今日の8つに共通していたのは、これでした。
GCが自動化してくれたのは、メモリの解放だけ。「何を、いつまで持つか」という判断は、いまも人間の設計の仕事です。
参照を握った責任は、GCには渡せない。そこを引き受けている人のコードが、長時間稼働に耐えるんですよね。
みなさんが踏んだメモリの事故、よかったら教えてください。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。