目次
- 1 1. はじめに
- 2 2. Java Heap(ヒープ領域)とは?
- 3 3. 「java heap space」エラーが発生する代表的な原因
- 4 4. ヒープサイズの確認方法
- 5 5. 解決法①:ヒープサイズを増やす
- 6 6. 解決法②:コードを最適化する
- 7 7. 解決法③:GC(ガーベジコレクション)をチューニングする
- 8 7-1. GC(ガーベジコレクション)とは何か?
- 9 7-2. GCの種類と特徴(選び方のポイント)
- 10 7-3. GCを明示的に切り替える方法
- 11 7-4. GCログを出力して、問題点を目視する
- 12 7-5. GCの遅延が「java heap space」の引き金になるケース
- 13 7-6. G1GC をチューニングするときのポイント
- 14 7-7. GCチューニングのまとめ
- 15 8. 解決法④:メモリリークを検出する
- 16 8-1. メモリリークとは?(Javaでも普通に起きる)
- 17 8-2. メモリリークの典型パターン
- 18 8-3. メモリリークの「兆候」を見抜くチェックポイント
- 19 8-4. ツール①:VisualVM でリークを目視確認する
- 20 8-5. ツール②:Eclipse MAT(Memory Analyzer Tool)で深堀り解析
- 21 8-6. “Dominator Tree” が分かれば解析が一気に進む
- 22 8-7. ヒープダンプ取得方法(コマンドライン編)
- 23 8-8. メモリリークの根本的な解決には「コード修正」が必要
- 24 8-9. 「ヒープ不足」か「リーク」かを見分けるポイント
- 25 8-10. まとめ:ヒープ調整で直らない OOM はリークを疑え
- 26 9. Docker / Kubernetes での「java heap space」問題と対策
- 27 9-1. なぜコンテナ環境では Heap Space エラーが多発するのか?
- 28 9-2. Java 8u191 以降と Java 11 以降の改善点
- 29 9-3. コンテナでヒープサイズを明示指定する方法(必須)
- 30 9-4. Kubernetes(K8s)でのメモリ設定の注意点
- 31 9-5. Java 11 以降の自動ヒープ割合(MaxRAMPercentage)
- 32 9-6. なぜコンテナで OOMKilled が多発するのか?(実例)
- 33 9-7. GCログやメトリクスでのコンテナ特有のチェックポイント
- 34 9-8. まとめ:コンテナ環境は「明示的設定」が基本
- 35 10. 避けるべきアンチパターン(NGコード・NG設定)
- 36 10-1. 無制限に増え続けるコレクションを放置する
- 37 10-2. 巨大なファイル・データを一気に読み込む
- 38 10-3. static 変数にデータを保持し続ける
- 39 10-4. Stream / Lambda を無計画に使い、中間リストを大量生成
- 40 10-5. String の連結を + 演算子で大量に行う
- 41 10-6. キャッシュを作りすぎて管理しない
- 42 10-7. メモリ上でログや統計を保持し続ける
- 43 10-8. Dockerコンテナで -Xmx を指定しない
- 44 10-9. GC設定の過剰なチューニング
- 45 10-10. まとめ:アンチパターンの多くは「無駄に溜める」ことが原因
- 46 11. 実例:このコードは危ない(典型的なメモリ問題のパターン)
- 47 11-1. 巨大なデータを一括で読み込むパターン
- 48 11-2. コレクション肥大化パターン
- 49 11-3. Stream API による中間オブジェクト大量生成
- 50 11-4. JSONやXMLを丸ごと一括パースする
- 51 11-5. 画像・バイナリデータを全てメモリに載せる
- 52 11-6. staticキャッシュによる無限保持
- 53 11-7. ThreadLocal の誤用
- 54 11-8. 例外を大量に生成する
- 55 11-9. まとめ:危険コードは「地味にヒープを削っていく」
- 56 12. Javaメモリ管理のベストプラクティス(再発防止に必須)
- 57 12-1. ヒープサイズは明示的に設定する(特に本番環境)
- 58 12-2. 適切に監視する(GC・メモリ使用量・OOM)
- 59 12-3. キャッシュは「制御されたキャッシュ」を使う
- 60 12-4. Stream API やラムダ式の使いすぎに注意する
- 61 12-5. 巨大ファイル・巨大データの扱いはストリーミングに切り替える
- 62 12-6. ThreadLocal を慎重に扱う
- 63 12-7. メモリリーク対策としてヒープダンプを定期的に取る
- 64 12-8. GC は必要最小限のチューニングに留める
- 65 12-9. そもそもアーキテクチャを分割するという選択肢
- 66 12-10. まとめ:Javaのメモリ管理は「積み重ねの最適化」が重要
- 67 13. まとめ:java heap space エラーを防ぐために押さえるべきポイント
- 68 13-1. エラーの本質は「ヒープが足りない」ではなく「なぜ足りないのか」
- 69 13-2. まず行うべき初期調査ステップ
- 70 13-3. 実務で非常に多い危険パターン
- 71 13-4. 根本対策は「システム設計」と「データ処理の最適化」
- 72 13-5. 読者に伝えたい最重要ポイント
- 73 13-6. Javaメモリ管理は“知っているだけで差が付く技術”
- 74 14. FAQ(よくある質問)
- 75 Q1. java.lang.OutOfMemoryError: Java heap space と
- 76 Q2. ヒープをとりあえず増やせば解決しますか?
- 77 Q3. Javaのヒープはどれくらい増やしてよいですか?
- 78 Q4. コンテナ(Docker / K8s)で Java が OOMKilled されるのはなぜですか?
- 79 Q5. メモリリークかどうかを簡単に判断する方法はありますか?
- 80 Q6. Eclipse / IntelliJ で設定したヒープが反映されません
- 81 Q7. Spring Boot はメモリを多く使うと聞きましたが本当ですか?
- 82 Q8. GC の種類はどれを使うべきですか?
- 83 Q9. Cloud Run / Lambda などのサーバーレス環境ではどう扱う?
- 84 Q10. Java のヒープ不足はどうすれば再発防止できますか?
- 85 まとめ:FAQで疑問を解消しながら、実務のメモリ対策へ
1. はじめに
Javaで開発をしていると、ある日突然コンソールにjava.lang.OutOfMemoryError: Java heap spaceというメッセージが出て、アプリケーションが落ちてしまう――
そんな経験をしたことはないでしょうか。 このエラーは、「Javaが使えるメモリ(ヒープ領域)が足りなくなった」 という意味です。
しかし、エラーメッセージだけ見ても、- 何が原因で足りなくなったのか
- どこをどう調整すればよいのか
- コードの問題なのか、設定の問題なのか
- GC(ガーベジコレクション)が重くなりレスポンスが悪化する
- サーバー全体のメモリが逼迫し、他プロセスに影響が出る
- 本質的なメモリリークが放置され、再度 OutOfMemoryError が発生する
1-1. この記事の想定読者
この記事は、次のような方を想定しています。- Javaの基本文法(クラス・メソッド・コレクションなど)は理解している
- しかし、JVMの中でメモリがどう管理されているかまではよく分かっていない
- 開発・テスト・本番のいずれかで「java heap space」や
OutOfMemoryErrorを経験した、あるいはこれから備えておきたい - Docker / コンテナ / クラウド環境で Java を動かしており、メモリ設定まわりに少し不安がある
1-2. この記事で分かること
この記事では、「java heap space」エラーについて、単なる対処方法の羅列ではなく、仕組みから順に 解説していきます。 主なポイントは次の通りです。- Javaヒープ領域とは何か
- Stack との違い
- オブジェクトがどこに割り当てられるのか
- 「java heap space」エラーが発生する代表的なパターン
- 大量データの一括読み込み
- コレクションやキャッシュの作りすぎ
- メモリリーク(参照が残り続けるコード)
- ヒープサイズの確認方法と増やし方
- コマンドラインオプション(
-Xms,-Xmx) - IDE(Eclipse / IntelliJ など)での設定
- アプリケーションサーバ(Tomcat など)の設定ポイント
- コマンドラインオプション(
- コード側でできるメモリ削減の工夫
- コレクションの使い方の見直し
- ストリーム・ラムダ式使用時の注意点
- 大量データを扱うときの分割処理
- GC(ガーベジコレクション)とヒープの関係
- GCの基本的な動き
- GCログを使った簡単な読み取り方
- メモリリークの検出とツール活用
- ヒープダンプの取得
- VisualVM や Eclipse MAT を使った解析の入り口
- Docker / Kubernetes などコンテナ環境での注意点
- コンテナと
-Xmxの関係 - cgroup によるメモリ制限と OOM Killer
- コンテナと
- 「とりあえずヒープを増やせばいいのか?」
- 「どこまでヒープを増やしてよいのか?」
- 「メモリリークかどうかをざっくり見分ける方法は?」
1-3. 記事の読み進め方
「java heap space」エラーは、- いますぐ本番障害を解決したい人
- これからトラブルを未然に防ぎたい人
- ヒープサイズの変更方法
- メモリリークのチェック方法
- 「Javaヒープとは何か」という基礎
- 代表的なエラー原因
- その上での解決策・チューニング手順
2. Java Heap(ヒープ領域)とは?
「java heap space」エラーを正しく理解するためには、まず Javaがどのようにメモリを管理しているか を知る必要があります。 Javaでは、メモリは用途に応じていくつかの領域に分かれており、その中でもヒープ領域は オブジェクトのためのメモリ置き場 という非常に重要な役割を担っています。2-1. Javaのメモリ構造の全体像
Javaアプリケーションは、JVM(Java Virtual Machine)上で動作します。 JVMは、さまざまなデータを扱うために複数のメモリ領域を持っており、代表的なものは次の3つです。■ メモリ領域の種類
- Heap(ヒープ) アプリケーションが生成したオブジェクトを格納する領域。 ここが不足すると「java heap space」エラーになる。
- Stack(スタック) メソッド呼び出し、ローカル変数、参照などを扱う領域。 ここが溢れると「StackOverflowError」になる。
- Method Area / Metaspace クラス情報、定数、メタデータ、JITコンパイル結果を保持する領域。
2-2. ヒープ領域の役割
Javaのヒープ領域は、次のようなものを保存する場所です。- new で生成されたオブジェクト
- 配列(List, Map などの中身も含む)
- ラムダ式が内部で生成するオブジェクト
- 文字列(String)や StringBuilder のバッファ
- コレクションフレームワーク内で使用されるデータ構造
2-3. ヒープ領域不足が起きるとどうなるか?
ヒープ領域が小さい、またはアプリケーションが大量のオブジェクトを生成すると、 Javaはヒープ内の不要なオブジェクトを削除するために GC(Garbage Collection) を実行します。 しかし、GCを何度繰り返しても十分にメモリが空かず、最終的にメモリ確保ができなくなると、java.lang.OutOfMemoryError: Java heap space
が発生してアプリケーションが強制終了します。2-4. 「ヒープを増やせばいい」は半分正解・半分不正解
確かに、ヒープサイズが小さすぎてエラーが出ている場合は、-Xms1024m -Xmx2048m
のように設定を増やすことで解決できます。 しかし、原因が メモリリーク や コード上の非効率な巨大データ処理 である場合は、
ヒープを増やしても一時的にしのげるだけで、根本解決にはなりません。つまり「なぜヒープが足りなくなっているのか」を理解することが何より重要です。
2-5. ヒープ内の構造(Eden / Survivor / Old領域)
Javaのヒープは大きく2つの領域に分かれています。- Young領域(新しく作られたオブジェクト)
- Eden
- Survivor(S0, S1)
- Old領域(長生きしたオブジェクト)
Young領域
オブジェクトはまず Eden に入り、短命なものはすぐに削除されます。 高頻度でGCが発生するが、処理は軽い。Old領域
長く生き続けて Young から昇格したオブジェクトが入る。 削除コストが高いGCが実行されるため、ここが増え続けると遅延や停止を引き起こす。 「Heap space エラー」は、最終的に Old領域があふれることで起きるケースが多いです。2-6. なぜヒープ不足は初心者・中級者に多いのか?
Javaはガーベジコレクションが自動で行われるため、 「メモリ管理は全部JVMがやってくれる」と思われがちです。 しかし、現実には、- 大量にオブジェクトを作り続けるコード
- コレクションに保持したままの参照
- ラムダ式やStreamで意図せず巨大データを生成
- キャッシュの作りすぎ
- Dockerコンテナでヒープ制限を誤解する
- IDEでのヒープ設定を間違える
3. 「java heap space」エラーが発生する代表的な原因
Javaのヒープ不足は多くの現場で頻出する問題ですが、その原因は大きく分けて 「データ量」「コード設計」「設定ミス」 の3つに分類できます。 このセクションでは、それぞれの典型的なパターンを整理しながら、なぜエラーにつながるのかを分かりやすく解説します。3-1. 大量データの読み込みによるメモリ圧迫
最も多いパターンが、扱うデータ量そのものが大きすぎてヒープを使い切るケース です。■ よくある例
- 巨大なCSV/JSON/XMLを 一気にメモリ上に読み込む
- データベースから大量のレコードを 全件 fetch してしまう
- Web API が返すレスポンスが大きい(画像データ・ログデータなど)
「パース前の文字列」と「パース後のオブジェクト」が同時にメモリ上に存在するケース」例えば 500MB の JSON を一度文字列として読み込み、 さらに Jackson などでデシリアライズすると、 最終的には 1GB 以上のメモリを消費 してしまうことがあります。
■ 対応の方向性
- 分割読み込み(ストリーミング処理) を導入する
- DBアクセスは ページング を使う
- 中間データを必要以上に保持しないようにする
3-2. コレクションにデータを溜め込みすぎる
初心者〜中級者に非常に多いのが、このパターンです。■ よくあるミス
Listにログや一時データをどんどん追加 → 削除されないまま肥大化Mapをキャッシュ代わりに使う(しかし破棄されない)- ループ内で新しいオブジェクトを作り続ける
- Stream API やラムダ式で一時オブジェクトを大量生成
■ 対応の方向性
- キャッシュは ライフサイクルを決める
- コレクションは 容量制限(上限) を決める
- 大量データを扱う仕組みは 定期的にクリアする
List<String> list = new ArrayList<>();
for (...) {
list.add(heavyData); // ← ここで永遠に増加
}このようなコードは非常に危険です。3-3. メモリリーク(意図しないオブジェクト保持)
JavaはGCがあるため「メモリリークとは無縁」と思われがちですが、 実際には Java でも普通にメモリリークは発生します。■ よくあるリークの発生ポイント
- 静的変数(static)にオブジェクトを保持したままにする
- Listener や Callback の登録解除忘れ
- Stream / Lambda 内で参照を残し続ける
- 長期稼働バッチでオブジェクトが蓄積
- ThreadLocal に大量のデータを入れ、スレッドが再利用されてしまう
■ 対応の方向性
- 静的変数の使い方を見直す
removeListener()やclose()を確実に呼ぶ- 長時間動く処理は ヒープダンプ を取って調査する
- ThreadLocal は必要な場面以外では使わない
3-4. JVMヒープサイズの設定不足(デフォルトが小さい)
アプリケーションは正常でも、ヒープ領域自体が小さすぎる ためにエラーが出るケースもあります。 デフォルトのヒープサイズは OS や Java 版数によって異なり、 Java 8 では一般的に 物理メモリの 1/64 ~ 1/4 程度 が割り当てられます。 しかし、多くの現場で見られるのが、-Xmx の指定がなく、しかも大量データを扱うという危険な状態。■ よくあるケース
- 本番だけデータ量が多く、デフォルトヒープでは不足
- Docker 上で動かしているが、
-Xmxを設定していない - Spring Boot を fat-jar で起動しており、デフォルト値のまま
■ 対応の方向性
- 適切な値で
-Xmsと-Xmxを設定する - コンテナ環境では 物理メモリと cgroup 制限 を理解して設定する
3-5. 再起動を含む処理でオブジェクトが残り続けるパターン
以下のようなアプリケーションは、メモリに負荷が蓄積しやすいです。- 長時間稼働する Spring Boot アプリ
- メモリを多用するバッチ処理
- 大量ユーザーがアクセスする Web アプリ
- メモリを使い切る
- GCでギリギリ回復
- しかし蓄積が残り、次回の処理で OOM
3-6. コンテナ(Docker / Kubernetes)での制限誤解
Docker や Kubernetes でよく発生する落とし穴があります。■ 落とし穴
-Xmxを設定しない → Java はコンテナではなくホストの物理メモリ量を参照 → 使いすぎ → OOM Killer により強制終了
■ 対応
-XX:MaxRAMPercentageを適切に設定-Xmxをコンテナのメモリに合わせる- Java 11 以降の “UseContainerSupport” の理解が必須
4. ヒープサイズの確認方法
「java heap space」エラーが発生したとき、 まず行うべきは “現在のヒープがどれくらい割り当てられているのか” を確認することです。 予想よりもヒープが小さいだけのケースも多いため、確認作業はトラブルシューティングの第一歩です。 このセクションでは、コマンドライン・プログラム内・IDE・アプリケーションサーバ など、 様々な場面でヒープサイズを確認する方法を紹介します。4-1. コマンドラインからヒープサイズを確認する
Javaには、起動時に JVM の設定値を確認するためのオプションがいくつか用意されています。■ -XX:+PrintFlagsFinal を利用する方法
最も確実にヒープサイズを確認できる方法がこれです。java -XX:+PrintFlagsFinal -version | grep HeapSize実行すると、次のような情報が表示されます。- InitialHeapSize … 「-Xms」で指定したヒープの初期サイズ
- MaxHeapSize … 「-Xmx」で指定したヒープの最大サイズ
uintx InitialHeapSize = 268435456
uintx MaxHeapSize = 4294967296上記は、- 初期ヒープ:256MB
- 最大ヒープ:4GB という意味になります。
■ 具体例
java -Xms512m -Xmx2g -XX:+PrintFlagsFinal -version | grep HeapSize設定値を変更した後にも使えるため、確実な確認手段です。4-2. 実行中のプログラム内からヒープサイズを確認する
時には、実行しているアプリケーションの中からヒープ量を確認したい ことがあります。 Javaでは、Runtime クラスを使うことで簡単にヒープ情報を取得できます。long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
long free = Runtime.getRuntime().freeMemory();
System.out.println("Max Heap: " + (max / 1024 / 1024) + " MB");
System.out.println("Total Heap: " + (total / 1024 / 1024) + " MB");
System.out.println("Free Heap: " + (free / 1024 / 1024) + " MB");- maxMemory() … 最大ヒープサイズ(-Xmx)
- totalMemory() … 現在JVMが確保しているヒープ
- freeMemory() … その中で利用可能な領域
4-3. VisualVM や Mission Control などのツールで確認する
GUI を使って視覚的にヒープを見る方法もあります。■ VisualVM
- ヒープ利用量のリアルタイム表示
- GC実行のタイミング
- ヒープダンプの取得
■ Java Mission Control(JMC)
- より詳細なプロファイリングが可能
- 特に Java 11 以降の運用で便利
4-4. Eclipse / IntelliJ の IDE で確認する
IDE 上でアプリを起動している場合、 IDE の設定でヒープサイズが変わることがあります。■ Eclipse の場合
ウィンドウ → 設定 → Java → インストール済みのJRE
又は
Run Configuration → VM arguments
に -Xms / -Xmx を設定。■ IntelliJ IDEA の場合
Help → Change Memory Settings
または
Run/Debug Configuration 内の VM options に -Xmx を追加。 IDEが独自にヒープサイズを制限している場合もあるため要注意です。4-5. アプリケーションサーバ(Tomcat / Jetty)での確認
Webアプリケーションの場合は、 アプリサーバの起動スクリプトでヒープが指定されていることがあります。■ Tomcat の例(Linux)
CATALINA_OPTS="-Xms512m -Xmx2g"
■ Tomcat の例(Windows)
set JAVA_OPTS=-Xms512m -Xmx2g
本番環境では、ここがデフォルトのままというケースも多く、
サービス開始後しばらくして heap space エラーが出る典型的パターンです。4-6. Docker / Kubernetes でのヒープ確認(重要)
コンテナ環境では、物理メモリ・cgroup・Javaの設定値 が複雑に絡みます。 特に Java 11 以降は「UseContainerSupport」により、自動でヒープを調整しますが、- メモリ制限(
--memory=512m) -Xmxの指定有無
docker run --memory=512m ...
のようにコンテナ側だけ制限して -Xmx を設定しないと- Java はホストメモリを参照 → 大きく確保しようとする
- cgroup が制限 → OOM Killer によって強制終了
4-7. まとめ:ヒープ確認は「最初の必須作業」
ヒープ不足は、原因によって対策が大きく異なります。 まずは、- 現在のヒープサイズ
- 実際の使用量
- ツールによる可視化
5. 解決法①:ヒープサイズを増やす
「java heap space」エラーの最も直接的な対処法が ヒープサイズの拡大 です。 原因が単純なメモリ不足であれば、ヒープを適切に増やすことでアプリケーションが正常に動作するようになります。 しかし、ヒープを増やす際には “正しい設定方法” と “注意点” を理解しておくことが重要です。 誤った設定はパフォーマンス低下や OOM(Out Of Memory)を引き起こす可能性があります。5-1. コマンドラインでヒープサイズを増やす
Javaアプリケーションを JAR で起動する場合、 最も基本となる方法は-Xms と -Xmx の指定です。■ 例:初期512MB、最大2GBに設定
java -Xms512m -Xmx2g -jar app.jar-Xms… JVM起動時に確保する初期ヒープサイズ-Xmx… JVMが使用できるヒープの最大値
-Xms と -Xmx を同じ値にすることで
ヒープの拡張にかかるオーバーヘッドを抑えることができます。 例:java -Xms2g -Xmx2g -jar app.jar5-2. サーバー常駐アプリ(Tomcat / Jetty など)の設定
Webアプリケーションの場合は、アプリケーションサーバの起動スクリプトで指定します。■ Tomcat(Linux)
setenv.sh に設定:export CATALINA_OPTS="$CATALINA_OPTS -Xms512m -Xmx2048m"
■ Tomcat(Windows)
setenv.bat に設定:set CATALINA_OPTS=-Xms512m -Xmx2048m
■ Jetty
start.ini か jetty.conf に下記を追加:--exec
-Xms512m
-Xmx2048mWebアプリはトラフィック量に応じてメモリ利用が急増するため、
本番は テストより余裕を持たせる のが基本です。5-3. Spring Boot アプリの設定
Spring Boot を fat-jar で動かす場合も、基本は同じです。java -Xms1g -Xmx2g -jar spring-app.jarSpring Boot の場合、起動時に多くのクラスや設定が読み込まれるため、 普通のJavaプログラムよりメモリを多く使う傾向があります。5-4. Docker / Kubernetes でのヒープ設定(重要)
コンテナ内のJavaは、コンテナ制限と JVM のヒープ計算方式 が絡むため注意が必要です。■ 推奨設定例(Docker)
docker run --memory=1g \
-e JAVA_OPTS="-Xms512m -Xmx800m" \
my-java-app■ なぜ -Xmx を明示する必要があるのか?
Docker では -Xmx を指定しないと…- JVM はコンテナではなく ホストマシンの物理メモリ を参考にヒープを決める
- その結果、コンテナが許容できるより大きな領域を確保しようとする
- cgroupによるメモリ制限に引っかかり、OOM Killer によってプロセスが殺される
-Xmx を設定することが必須 です。5-5. CI/CD やクラウド環境でのヒープ設定例
クラウド上の Java 実行環境では、メモリ量に応じて次のように設定するのが一般的です。| メモリ総量 | 推奨ヒープ(目安) |
|---|---|
| 1GB | 512〜800MB |
| 2GB | 1.2〜1.6GB |
| 4GB | 2〜3GB |
| 8GB | 4〜6GB |
5-6. ヒープを増やせば解決? → 限界もある
ヒープを増やすことで一時的にエラーが解消される場合もありますが、 以下のようなケースでは根本解決になりません。- メモリリークが発生している
- コレクションが永遠に膨らむ
- 大量データを一括処理する構造
- アプリが “正しくない設計” になっている
6. 解決法②:コードを最適化する
ヒープサイズを増やすことは有効な対処の一つですが、 根本原因が コードの構造やデータ処理の方法にある場合、 設定だけでは「java heap space」エラーは再発します。 このセクションでは、特に実務で多い “メモリを無駄遣いしてしまうコードのパターン” と、 それを改善するための具体的なアプローチを解説します。6-1. コレクションの扱い方を見直す
Javaのコレクション(List、Map、Set など)は便利ですが、 不用意に使うと メモリ増加の主原因 になります。■ パターン①:List / Map が無限に増え続ける
よくある例:List<String> logs = new ArrayList<>();
while (true) {
logs.add(fetchLog()); // ← 永遠に増える
}このように、明確な終了条件や上限がないコレクション は、
長時間運用すると確実にヒープを圧迫します。● 改善案
- 上限付きのコレクションを使う(例:サイズを決めて古いデータを捨てる)
- 不要な値を定期的にクリアする
- キャッシュ代わりに Map を使うなら、Evict(削除)機能付きのキャッシュ を採用する → Guava Cache や Caffeine が有効
■ パターン②:初期容量を指定しないコレクション
ArrayList や HashMap は、容量を超えると自動拡張しますが、 この処理で 新たな配列を確保 → コピー → 古い配列の破棄 が行われます。 大量データを扱う場合、初期容量を指定しないのは効率が悪くメモリを浪費します。● 改善例:
List<String> items = new ArrayList<>(10000);「想定されるサイズを知っている」場合は、最初から設定したほうがベターです。6-2. 大量データを一括で処理しない(分割処理)
大量データをまとめて処理すると、 全データがヒープに載る → OOM という最悪のパターンに陥りがちです。■ 悪い例(巨大ファイルを全読み込み)
String json = Files.readString(Paths.get("large.json"));
Object data = new ObjectMapper().readValue(json, Data.class);■ 改善案
- ストリーミング処理 を使う(Jackson の Streaming API など)
- 小分けに読み込む(バッチのページング)
- ストリームを逐次処理して 保持しない
● 例:Jackson Streaming で巨大JSONを処理
JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
while (!parser.isClosed()) {
JsonToken token = parser.nextToken();
// 必要な処理だけ実行し、メモリに保持しない
}
}6-3. 不必要なオブジェクト生成を避ける
Stream やラムダ式は便利ですが、 内部で一時的なオブジェクトを大量生成することがあります。■ 悪い例(Streamで巨大中間リストを生成)
List<Result> results = items.stream()
.map(this::toResult)
.collect(Collectors.toList());ここで items が巨大 だと、
中間オブジェクトが一時的に大量生成され、ヒープが膨れます。● 改善案
- for ループで逐次処理する
- 必要な結果だけ処理して即書き出す(保存しない)
collect()を避けるか、自前で制御する
6-4. String の連結に注意
Javaの String は immutable(不変)のため、 連結するたびに 新しいオブジェクトが生成 されます。■ 改善案
- 大量連結は
StringBuilderを使う - ログ生成時に不要な文字列連結が発生しないようにする
StringBuilder sb = new StringBuilder();
for (String s : items) {
sb.append(s);
}6-5. キャッシュの作りすぎに注意
特に Web アプリやバッチ処理でありがちなケースです。- 「高速化のためにキャッシュを作った」
- → しかしクリアを忘れる
- → キャッシュが徐々に肥大化
- → ヒープ不足 → OOM
■ 改善策
- キャッシュは TTL(時間) や 最大サイズ を設定する
ConcurrentHashMapなどをキャッシュ代わりに使うのは危険- Caffeine などメモリ制御がしっかりしたキャッシュを使う
6-6. 大量ループの中でオブジェクトを再生成しない
■ 悪い例
for (...) {
StringBuilder sb = new StringBuilder(); // 毎回生成
...
}このケースでは、必要以上に多くの一時オブジェクト が作られてしまいます。● 改善案:
StringBuilder sb = new StringBuilder();
for (...) {
sb.setLength(0); // 再利用
}6-7. メモリを消費する処理を別プロセスに分ける
Javaで巨大データを扱う場合、 アプリケーションのアーキテクチャ自体を見直す必要がある場合もあります。- ETL 処理を別バッチに分離
- 分散処理基盤(Spark や Hadoop)に任せる
- サービス分割してヒープの競合を避ける
6-8. コード最適化は再発防止の重要ステップ
ヒープを増やすだけでは、 いつか再び “限界” が来て同じエラーが発生します。 「java heap space」エラーの根本的な防止には、- データ量の把握
- オブジェクト生成の見直し
- コレクション設計の改善
7. 解決法③:GC(ガーベジコレクション)をチューニングする
「java heap space」エラーは、単にヒープが小さい場合だけでなく、 GC が十分にメモリを回収できず、結果としてヒープが逼迫していくケース でも発生します。 GC を理解していないと、 「メモリはあるはずなのにエラーが出る」「処理が極端に遅くなる」 といった症状の原因を見誤ることがよくあります。 このセクションでは、JavaのGCの基本的な仕組みから、 実務で役立つチューニングのポイントまでを分かりやすく解説します。7-1. GC(ガーベジコレクション)とは何か?
GC は、Java が自動で不要なオブジェクトを破棄する仕組みです。 Javaのヒープ領域は大きく 2つの世代に分かれており、それぞれ異なるGCが行われます。● Young領域(短命オブジェクト)
- Eden / Survivor(S0, S1)
- ローカルで生成された一時データなど
- 頻繁にGCが行われるが軽い
● Old領域(長生きオブジェクト)
- Youngから昇格したオブジェクト
- GCは重く、頻繁に起こるとアプリが固まる
「java heap space」は最終的に Old領域があふれることで起きることが多い。
7-2. GCの種類と特徴(選び方のポイント)
Java には複数のGC方式が存在します。 用途に応じて使い分けることで、パフォーマンスが大幅に改善されます。● ① G1GC(Java 9以降のデフォルト)
- 全体を小さな領域に分割し、少しずつ回収する方式
- 停止時間(Stop-The-World)を短くできる
- Webアプリ・業務システムに最適
● ② Parallel GC(大量バッチ処理向け)
- 並列化されて高速
- ただし停止時間が長くなることがある
- CPUを多く使うバッチ処理などで有利
● ③ ZGC(ミリ秒単位の低遅延GC)
- Java 11 以降で利用可能
- 遅延に敏感なアプリ(ゲームサーバー・HFT)向け
- 大規模ヒープ(数十GB)でも有効
● ④ Shenandoah(低遅延GC)
- Red Hat 系ディストリビューション向け
- 停止時間を極限まで短縮可能
- AWS Corretto でも利用可能
7-3. GCを明示的に切り替える方法
G1GCを使うのが基本ですが、目的に応じて指定もできます。# G1GC
java -XX:+UseG1GC -jar app.jar
# Parallel GC
java -XX:+UseParallelGC -jar app.jar
# ZGC
java -XX:+UseZGC -jar app.jarGC方式によって、ヒープ利用状況や停止時間が大きく変わるため、
本番システムでは明示的に設定することも多いです。7-4. GCログを出力して、問題点を目視する
GCがどのくらいメモリを回収しているか、 Stop-The-World がどの程度発生しているかを把握することは非常に重要です。● GCログ出力の基本設定
java \
-Xms1g -Xmx1g \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:gc.log \
-jar app.jar生成された gc.log を見ると、- Young GC が多すぎる
- Old領域がまったく減らない
- Full GC が頻発している
- 1回のGCで回収量が極端に少ない
7-5. GCの遅延が「java heap space」の引き金になるケース
ヒープ不足の原因が次のようなパターンの場合、 GCの挙動が決定的なヒントになります。● 症状
- アプリが急に固まる
- GCが数秒〜数十秒実行されている
- Old領域が増え続ける
- Full GC が増え、最後に OOM 発生
■ 主な原因
- メモリリーク
- 永久的に保持されたコレクション
- オブジェクトの寿命が長すぎる
- Old領域の肥大化
7-6. G1GC をチューニングするときのポイント
G1GCは優秀ですが、チューニングでさらに安定させられます。● 代表的なパラメータ
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=45- MaxGCPauseMillis → 停止時間の目標値(200msなど)
- G1HeapRegionSize → G1が使うヒープ分割のサイズ
- InitiatingHeapOccupancyPercent → Old領域が何%になったらGCを開始するか
7-7. GCチューニングのまとめ
GCの改善は、ヒープサイズを増やすだけでは分からない- オブジェクトの寿命
- コレクションの扱い
- メモリリークの有無
- ヒープの逼迫ポイント
8. 解決法④:メモリリークを検出する
ヒープを増やしても、コードを最適化しても、 それでも 「java heap space」エラーが再発する」 という場合、 もっとも疑うべきは メモリリーク(Memory Leak) です。 JavaはGCがあるためメモリリークが起きにくいと思われがちですが、 実際には現場で 最も厄介で再発しやすい原因 がこのメモリリークです。 ここでは、メモリリークの理解から、 実務で役立つ解析ツール(VisualVM / Eclipse MAT)まで、 “明日から使える実践手順” を中心に解説します。8-1. メモリリークとは?(Javaでも普通に起きる)
Javaのメモリリークとは、不要なオブジェクトに参照が残り続け、GC が回収できない状態のことです。 Garbage Collection がある Java でも、
staticにオブジェクトを保持したまま- 動的に追加した listener が解除されていない
- コレクションが肥大化して参照を持ち続ける
- ThreadLocal にデータが残り続ける
- フレームワークのライフサイクルと噛み合わない
8-2. メモリリークの典型パターン
● ① コレクションの肥大化(最も多い)
List / Map / Set などに追加し続け、削除しないケース。 Java業務システムの OOM 発生原因の半数以上がこれです。● ② static 変数に保持し続ける
private static List<User> cache = new ArrayList<>();
これがリークの出発点になるパターンは多いです。● ③ Listener / Callback の unregister 忘れ
GUI、Observer、イベントリスナーなど背景で参照が残るケース。● ④ ThreadLocal の誤用
スレッドプール環境では、ThreadLocal の値が 永続化してしまう 場合があります。● ⑤ 外部ライブラリが保持する参照
アプリ側では管理しづらい “隠れメモリ” もあり、ツール解析が必須。8-3. メモリリークの「兆候」を見抜くチェックポイント
以下の兆候がある場合は、ほぼ確実にメモリリークを疑うべきです。- Old領域だけが徐々に増加している
- Full GC が増えている
- Full GC 後もほとんどメモリが減らない
- 稼働時間に比例してヒープ使用量が増える
- 本番だけ長時間運用で落ちる
8-4. ツール①:VisualVM でリークを目視確認する
VisualVM は JDK に同梱されていることもあり、 最初の解析ツールとして非常に使いやすいです。● VisualVM でできること
- メモリ使用量のリアルタイム監視
- Old領域の増加を確認
- GC の頻度
- スレッドの監視
- ヒープダンプの取得
● ヒープダンプ取得の方法
VisualVM の「Monitor」タブ → “Heap Dump” ボタンを押すだけ。 取得したヒープダンプは、そのまま Eclipse MAT に渡して解析できます。8-5. ツール②:Eclipse MAT(Memory Analyzer Tool)で深堀り解析
「Javaのメモリリーク解析ツールといえばこれ」 というほど業界標準なのが Eclipse MAT です。● MAT で分かること
- どのオブジェクトがメモリを最も使っているか
- どの参照パスのせいでオブジェクトが残っているか
- オブジェクトが解放されない原因
- コレクションの肥大化
- Leak Suspects(疑いのある箇所)を自動表示
● 基本的な解析手順
- ヒープダンプファイル(*.hprof)を開く
- 「Leak Suspects Report」を実行する
- 大量にメモリを保持するコレクションを探す
- Dominator Tree を確認し“親オブジェクト”を特定する
- 参照パス(Path to GC Root)をたどる
8-6. “Dominator Tree” が分かれば解析が一気に進む
Dominator Tree は、 メモリ使用量をまとめて支配しているオブジェクト を特定するためのツリーです。 例:- 巨大な
ArrayList - キー数が膨大な
HashMap - 解放されていないキャッシュ
- static が握っている Singleton
8-7. ヒープダンプ取得方法(コマンドライン編)
Java には jmap コマンドでヒープダンプを取得する方法もあります。jmap -dump:format=b,file=heap.hprof <PID>または強制的に OOM が発生した際、自動でヒープダンプを吐く設定もあります。-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof本番環境の障害調査では必須の設定です。8-8. メモリリークの根本的な解決には「コード修正」が必要
リークが起きている場合、- ヒープを増やす
- GCをチューニングする
- 参照を持ち続けている箇所を修正
- コレクション設計を見直す
- static の使いすぎを避ける
- キャッシュを削除する仕組みを入れる

8-9. 「ヒープ不足」か「リーク」かを見分けるポイント
● ヒープ不足の場合
- データ量が増えるとすぐ OOM
- 処理量に比例する
- ヒープ増加で安定する
● メモリリークの場合
- 長時間運用すると OOM
- リクエストが増えると徐々に遅くなる
- Full GC の後もメモリが減らない
- ヒープ増加では解決しない
8-10. まとめ:ヒープ調整で直らない OOM はリークを疑え
「java heap space」トラブルの中で最も原因特定に時間がかかるのが メモリリーク です。 しかし、VisualVM × Eclipse MAT を使えば、- 大量にメモリを使うオブジェクト
- 解放されていない参照の根っこ
- コレクション肥大の発生源
9. Docker / Kubernetes での「java heap space」問題と対策
近年のJavaアプリケーションは、オンプレ環境だけでなく Docker や Kubernetes(K8s) 上で稼働するケースが主流になっています。 しかし、コンテナ環境では メモリ計算の仕組みがホストと異なる ため、 Java開発者が誤解しやすいポイントが多く、 「java heap space」や OOMKilled(コンテナ強制終了) が非常に発生しやすい状況になっています。 このセクションでは、コンテナ特有のメモリ管理の仕組みと、 実務で必ず抑えておくべき設定ポイントを分かりやすくまとめます。9-1. なぜコンテナ環境では Heap Space エラーが多発するのか?
理由はシンプルで、Javaがコンテナのメモリ制限を正しく認識しないことがあるためです。
● よくある誤解
「Docker でメモリ制限--memory=512m を指定したから、Javaも512MB内で動くだろう」 → 実際には違います。 Javaはヒープサイズを決めるとき、 コンテナではなくホストの物理メモリを参照してしまう 場合があります。 結果として、- Java は “ホストメモリが十分ある” と判断する
- ヒープサイズを大きめに確保しようとする
- コンテナの制限を超えた瞬間に OOM Killer が走り、強制終了
9-2. Java 8u191 以降と Java 11 以降の改善点
Java 8 の一部以降、および Java 11 以降では、 「UseContainerSupport(コンテナ認識機能)」が追加されています。● コンテナ内での動作
- cgroup による制限を認識できる
- その制限内でヒープサイズを自動計算
9-3. コンテナでヒープサイズを明示指定する方法(必須)
● 推奨起動方法
docker run \
--memory=1g \
-e JAVA_OPTS="-Xms512m -Xmx800m" \
my-java-appポイント:- コンテナメモリは 1GB
- Javaヒープは 800MB以内に抑える
- 残りはスレッドスタックやネイティブメモリに使われる
● ダメな例(よくある)
docker run --memory=1g my-java-app # -Xmx なし→ Java がホストメモリを参照してヒープを確保しようとし、
1GBを超えた時点で OOMKilled。9-4. Kubernetes(K8s)でのメモリ設定の注意点
Kubernetes ではresources.limits.memory の設定が重要です。● Pod の設定例
resources:
limits:
memory: "1024Mi"
requests:
memory: "512Mi"この場合、Javaの -Xmx は 800MB ~ 900MB程度 に抑えるのが安全です。● なぜリミットより小さく設定するのか?
理由は、Javaが使用するのはヒープだけではないためです。- ネイティブメモリ
- スレッドスタック(数百KB × スレッド数)
- Metaspace
- GC ワーカー
- JITコンパイルコード
- ライブラリ読み込み
「limit = X なら、-Xmx は X × 0.7 〜 0.8 が安全」というのが実務上の鉄則です。
9-5. Java 11 以降の自動ヒープ割合(MaxRAMPercentage)
Java 11 では、ヒープサイズが以下のルールで自動計算されます。● デフォルト設定
-XX:MaxRAMPercentage=25
-XX:MinRAMPercentage=50これはつまり、- 使用可能メモリの 25% を上限にヒープを確保
- 少なすぎる環境では最低でも 50% はヒープにする
● 推奨設定
コンテナ環境では MaxRAMPercentage を直接指定 したほうが安全です。 例:JAVA_OPTS="-XX:MaxRAMPercentage=70"9-6. なぜコンテナで OOMKilled が多発するのか?(実例)
本番でよくあるパターン:- K8sで memory limit = 1GB
- Java の
-Xmxを設定していない - Javaはホストのメモリを参照し、ヒープを 1GB以上確保しようとする
- OSがコンテナを強制終了 → OOMKilled
9-7. GCログやメトリクスでのコンテナ特有のチェックポイント
コンテナ環境では以下を重点的に確認します。- Pod 再起動が増えていないか
- OOMKilled のイベントが記録されていないか
- Old領域が増え続けていないか
- GC の回収量が極端に少ないタイミングがないか
- そもそもヒープ以外のネイティブメモリが不足していないか
9-8. まとめ:コンテナ環境は「明示的設定」が基本
--memory制限だけでは Java は正しくヒープを計算しないことがある-Xmxを必ず設定する- Nativeメモリやスレッドスタックも考慮して余裕を持つ
- Kubernetes の limit より小さい値を設定する
- Java 11 以上では MaxRAMPercentage の活用も有効
10. 避けるべきアンチパターン(NGコード・NG設定)
「java heap space」エラーは、単にヒープが不足したときだけでなく、 ある種の“危険な書き方”や“間違った設定”が原因で発生する ことがよくあります。 ここでは、実務で特に多く見られる “やってはいけないアンチパターン” を整理します。10-1. 無制限に増え続けるコレクションを放置する
最も頻発する問題が コレクションの肥大化 です。● NG例:上限なくリストにデータを追加していく
List<String> logs = new ArrayList<>();
while (true) {
logs.add(getMessage()); // ← 永遠に増える
}たったこれだけで、長時間運用すれば簡単に OOM に到達します。● なぜ危険か?
- GC がメモリを回収できず Old 領域が肥大化
- Full GC 多発 → アプリが固まりやすい
- 大量オブジェクトのコピーでCPU負荷も増大
● 回避策
- サイズ上限を設ける(LRUキャッシュなど)
- 定期的なクリア
- 不必要な保持は行わない
10-2. 巨大なファイル・データを一気に読み込む
これはバッチやサーバーサイド処理でよくやってしまいがちなミスです。● NG例:巨大JSONを丸ごと読み込む
String json = Files.readString(Paths.get("large.json"));
Data d = mapper.readValue(json, Data.class);● 問題点
- パース前・パース後の両方をメモリ上に保持
- 500MBのファイルが倍以上のメモリを圧迫
- さらに中間オブジェクトが生成され、ヒープが枯渇
● 回避策
- ストリーミング(逐次処理)を使う
- 一括処理ではなく分割読み込み
- メモリ上に永続保持しない
10-3. static 変数にデータを保持し続ける
● NG例:
public class UserCache {
private static Map<String, User> cache = new HashMap<>();
}● なぜ危険か?
- static は JVM が終了するまで存在し続ける
- キャッシュとして使うと解放されない
- 参照が残り、メモリリークの温床になる
● 回避策
- static の利用は最小限に
- キャッシュは専用フレームワーク(Caffeineなど)を使う
- TTL やサイズ上限を設定
10-4. Stream / Lambda を無計画に使い、中間リストを大量生成
Stream API は便利ですが、 内部で中間オブジェクトが生成され、メモリに負荷がかかることがあります。● NG例(collect が巨大な中間リストを作る)
List<Item> result = items.stream()
.map(this::convert)
.collect(Collectors.toList());● 回避策
- for-loop で逐次処理する
- 不要な中間リストを生成しない
- データ量が大きい場合は Stream 使用を再検討
10-5. String の連結を + 演算子で大量に行う
String は不変のため、連結するたびに新しい String が生成されます。● NG例
String result = "";
for (String s : list) {
result += s;
}● 問題点
- 毎回 String を新規生成
- インスタンスが大量に生まれ、メモリを圧迫
● 回避策
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}10-6. キャッシュを作りすぎて管理しない
● NG例
- APIレスポンスを Map に溜め込む
- 画像やファイルデータをキャッシュし続ける
- LRUなどの制御がない
● 危険ポイント
- 時間とともに肥大化
- GCで回収されない領域が増加
- 本番では必ず問題になる
● 回避策
- Caffeine / Guava Cache を使う
- 上限サイズをつける
- TTL(有効期限)を設定
10-7. メモリ上でログや統計を保持し続ける
● NG例
List<String> debugLogs = new ArrayList<>();
debugLogs.add(message);本番ではログをファイルに書き込むべきで、
メモリに保持する運用は危険です。10-8. Dockerコンテナで -Xmx を指定しない
これは近年のトラブルの大半を占めます。● NG例
docker run --memory=1g my-app● 問題点
- Java はホストメモリを参照してヒープを自動設定
- コンテナの制限を超えた瞬間に OOMKilled
● 回避策
docker run --memory=1g -e JAVA_OPTS="-Xmx700m"10-9. GC設定の過剰なチューニング
誤ったチューニングは逆効果になることがあります。● NG例
-XX:MaxGCPauseMillis=10
-XX:G1HeapRegionSize=1m極端な設定はGCを過剰にしたり、逆に回収が追いつかなくなります。● 回避策
- 基本は デフォルト設定で十分
- 問題があるときだけ、最小限のチューニングを行う
10-10. まとめ:アンチパターンの多くは「無駄に溜める」ことが原因
紹介したアンチパターンに共通するのは、“必要以上にオブジェクトを溜め込む”という動作です。
- 無限コレクション
- 不要な保持
- 一括読み込み
- static 設計
- キャッシュ暴走
- 中間オブジェクトの大量発生
11. 実例:このコードは危ない(典型的なメモリ問題のパターン)
ここでは「java heap space」エラーにつながりやすい、 実務で頻繁に遭遇する危険なコード例 を紹介し、 それぞれについて「なぜ危険なのか」「どう改善すべきか」を具体的に示します。 実際の現場では、これらのパターンが複合的に発生していることも多く、 コードレビューや障害調査でも非常に役立つ章です。11-1. 巨大なデータを一括で読み込むパターン
● NG例:巨大CSVファイルを全行読み込む
List<String> lines = Files.readAllLines(Paths.get("big.csv"));
● なぜ危険か?
- ファイルサイズが大きいほどメモリを圧迫
- 100MB の CSV でもパース前後で倍以上のメモリを使う
- 大量レコードの保持により Old 領域が枯渇
● 改善例:ストリーム(逐次処理)で読む
try (Stream<String> stream = Files.lines(Paths.get("big.csv"))) {
stream.forEach(line -> process(line));
}→ メモリ上には常に1行しか載らないため、非常に安全。11-2. コレクション肥大化パターン
● NG例:重いデータをListに溜め続ける
List<Order> orders = new ArrayList<>();
while (hasNext()) {
orders.add(fetchNextOrder());
}● なぜ危険か?
- Listの容量が増えるたびに内部配列を再確保
- 全件保持する必要がない場合は無駄
- 長時間運用すると Old領域を大量消費
● 改善例:逐次処理+必要に応じてバッチに分割
while (hasNext()) {
Order order = fetchNextOrder();
process(order); // 保持せず処理する
}もしくはList<Order> batch = new ArrayList<>(1000);
while (hasNext()) {
batch.add(fetchNextOrder());
if (batch.size() == 1000) {
processBatch(batch);
batch.clear();
}
}11-3. Stream API による中間オブジェクト大量生成
● NG例:map → filter → collect で中間リストを連発
List<Data> result = list.stream()
.map(this::convert)
.filter(d -> d.isValid())
.collect(Collectors.toList());● なぜ危険か?
- 内部で多くの一時リスト・オブジェクトを生成
- 特に巨大リストの処理はヒープを圧迫
- パイプラインが深いほど危険
● 改善例:for-loop に戻す or 逐次処理
List<Data> result = new ArrayList<>();
for (Item item : list) {
Data d = convert(item);
if (d.isValid()) {
result.add(d);
}
}11-4. JSONやXMLを丸ごと一括パースする
● NG例
String json = Files.readString(Paths.get("large.json"));
Data data = mapper.readValue(json, Data.class);● 危険な理由
- JSON文字列(raw)とデシリアライズ後のオブジェクトが両方メモリに残る
- 100MB級のファイルでは一瞬でヒープが埋まる
- Stream API でも同様の問題が起きることがある
● 改善例:Streaming API の利用
JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
while (!parser.isClosed()) {
JsonToken token = parser.nextToken();
// 必要なときだけ処理し、保持しない
}
}11-5. 画像・バイナリデータを全てメモリに載せる
● NG例
byte[] image = Files.readAllBytes(Paths.get("large.png"));● 危険性
- バイナリデータは構造が大きいことが多い
- 画像処理アプリでは OOM の主要原因
● 改善案
- バッファリングを使って処理する
- メモリ保持せず、ストリームとして処理する
- 数百万行ログの一括読み込みも同じく危険
11-6. staticキャッシュによる無限保持
● NG例
private static final List<Session> sessions = new ArrayList<>();● 問題点
- JVMが終了するまで sessions が解放されない
- 接続数に比例して膨張 → OOM
● 改善例
- サイズ管理されたキャッシュを使う (Caffeine, Guava Cache など)
- セッションのライフサイクルを明確に管理する
11-7. ThreadLocal の誤用
● NG例
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));ThreadLocal 自体は有用ですが、
スレッドプールと併用すると値が残り続け、リークの原因に。● 改善例
- ThreadLocal は短命にする
- 必要な場面以外では使用を避ける
remove()を呼んでクリアする
11-8. 例外を大量に生成する
意外と知られていませんが、 例外(Exception)はスタックトレース生成のため 非常に重いオブジェクト です。● NG例
for (...) {
try {
doSomething();
} catch (Exception e) {
// ログだけ
}
}→ 例外を乱発するとメモリを圧迫する● 改善策
- 通常処理を例外で制御しない
- バリデーションで弾く
- 必要なケース以外で例外を投げない
11-9. まとめ:危険コードは「地味にヒープを削っていく」
これらの例から見える共通点は、 “少しずつヒープを逼迫する構造が積み重なっている” 点です。- 一括読み込み
- 無限コレクション
- 解除忘れ
- 中間オブジェクト生成
- 例外多発
- static 保持
- ThreadLocal残存
12. Javaメモリ管理のベストプラクティス(再発防止に必須)
ここまで、「java heap space」エラーを引き起こす原因や、 ヒープ拡張・コード改善・GCチューニング・リーク調査などの対策を細かく解説してきました。 このセクションでは、それらを踏まえて 実務で確実に効果がある再発防止策(ベストプラクティス) をまとめます。 Javaアプリを安定して稼働させるための「最低限これだけは守るべき」という内容です。12-1. ヒープサイズは明示的に設定する(特に本番環境)
デフォルト設定のまま運用するのは、本番では非常に危険です。● ベストプラクティス
-Xmsと-Xmxを 明示的に指定- デフォルトのまま運用しない
- 開発・本番でヒープサイズを揃える(予期せぬ差異を防止)
-Xms1g -Xmx1g特に Docker / Kubernetes 環境では、 コンテナ制限を考慮してヒープを小さめに設定 する必要があります。12-2. 適切に監視する(GC・メモリ使用量・OOM)
ヒープ問題は、早期に兆候を掴めれば回避できます。● 監視すべき項目
- Old領域使用量
- Young領域の増加傾向
- Full GCの頻度
- GCの停止時間(Pause Time)
- コンテナの OOMKilled イベント
- Pod の再起動回数(K8s)
● 推奨ツール
- VisualVM
- JDK Mission Control
- Prometheus + Grafana
- Cloud Provider のメトリクス(CloudWatch 等)
12-3. キャッシュは「制御されたキャッシュ」を使う
キャッシュ暴走は実務の OOM で最も多い原因の一つです。● ベストプラクティス
- Caffeine / Guava Cache を使う
- TTL(有効期限)を必ず設定する
- 最大サイズ(例:1000件)を設定する
- 静的キャッシュは極力使わない
Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();12-4. Stream API やラムダ式の使いすぎに注意する
大量データを扱う場面では、Streamの連鎖は中間オブジェクトを増やします。● ベストプラクティス
- 必要以上に map/filter/collect を連鎖させない
- 巨大データは for-loop で逐次処理
- collect を使うときはデータ量を意識する
12-5. 巨大ファイル・巨大データの扱いはストリーミングに切り替える
大量データの一括処理は、ヒープ問題の根源です。● ベストプラクティス
- CSV → Files.lines()
- JSON → Jackson Streaming
- DB → ページング
- API → 分割取得(cursor/pagination)
12-6. ThreadLocal を慎重に扱う
ThreadLocal は非常に強力ですが、誤用すると致命的なメモリリークを引き起こします。● ベストプラクティス
- スレッドプールと併用する場合は特に注意
- 値を使い終わったら
remove() - 長寿命データを入れない
- static ThreadLocal は極力避ける
12-7. メモリリーク対策としてヒープダンプを定期的に取る
長期稼働するシステム(Webアプリ / バッチ / IoT)では、定期的にヒープダンプを取得して比較するとリークの初期兆候がつかめます。● 手段
- VisualVM
- jmap
-XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprofOOM 発生時の自動ダンプは必須設定です。12-8. GC は必要最小限のチューニングに留める
「GCをいじれば性能が上がるだろう」という誤解は危険です。● ベストプラクティス
- まずは デフォルト設定で運用
- 問題が生じたときに最小限の調整
- G1GC を基本選択
- ヒープを増やすほうが有効なことも多い
12-9. そもそもアーキテクチャを分割するという選択肢
データが巨大になりすぎたり、アプリがモノリシックすぎてヒープを大量に必要とする場合、- マイクロサービス化
- データ処理バッチを分割
- メッセージキュー(Kafka等)で分離
- 分散処理(Spark等)
12-10. まとめ:Javaのメモリ管理は「積み重ねの最適化」が重要
Javaの heap space 問題は、一つの設定や一つの修正だけで解決することは稀です。● 覚えておくべき要点
- ヒープ設定は必ず明示する
- 監視が最も重要
- コレクション肥大化を許さない
- 大量データはストリーミング
- キャッシュは管理する
- ThreadLocal は慎重に
- 必要ならツールでリーク解析
- コンテナ環境は別ルールで考える
13. まとめ:java heap space エラーを防ぐために押さえるべきポイント
本記事では「java heap space」エラーについて、原因から対処法、そして再発防止まで幅広く解説してきました。 このセクションでは、要点を整理し、実務で迷わないための“総まとめ”として簡潔に振り返ります。13-1. エラーの本質は「ヒープが足りない」ではなく「なぜ足りないのか」
java heap space は単なるメモリ不足ではありません。● 原因の本質は以下のいずれか:
- ヒープサイズが小さい(設定不足)
- 大量データの一括処理(設計上の問題)
- コレクション肥大化(削除や設計の欠如)
- メモリリーク(参照が残り続けてしまう)
- コンテナ環境での誤設定(Docker/K8s特有の問題)
13-2. まず行うべき初期調査ステップ
① ヒープサイズが適切か確認
→-Xms / -Xmx を明示設定する② 実行環境のメモリ制約を把握
→ Docker / Kubernetes では limit とヒープの整合が必須 →-XX:MaxRAMPercentage の確認も重要③ GCログを取得して観察
→ Old 領域の増加、Full GC 多発は危険サイン④ ヒープダンプを取って解析
→ VisualVM / MAT でリークの根拠を掴む13-3. 実務で非常に多い危険パターン
本記事で紹介したように、特に以下のパターンは本番障害に直結します。- 巨大ファイルの一括処理
- List / Map に上限なく追加する
- キャッシュ暴走
- static に溜め込む
- ストリームの連鎖で中間オブジェクト大量生成
- ThreadLocal の使い方を誤る
- Docker で -Xmx を指定しない
13-4. 根本対策は「システム設計」と「データ処理の最適化」
● システム全体で見るべきポイント
- 大量データは ストリーミング処理 に切り替える
- キャッシュは TTL・上限・削除機能 を備えた仕組みを使う
- 長期稼働アプリでは 定期的なメモリ監視 を行う
- リークの予兆は 早めにツールで解析 する
● それでも難しい場合は:
- バッチとオンライン処理の分離
- マイクロサービス化
- 分散処理基盤(Spark・Flink等)の採用
13-5. 読者に伝えたい最重要ポイント
最後に、本記事の中で特に重要な項目を3つだけ挙げるなら——✔ ヒープは必ず “明示” して設定する
✔ 大量データは “一括処理しない”
✔ メモリリークは “ヒープダンプ” でしか分からない
この3つを押さえるだけでも、「java heap space」エラーによる致命的な本番障害を大幅に減らすことができます。13-6. Javaメモリ管理は“知っているだけで差が付く技術”
Javaのメモリ管理は難しいように思えるかもしれませんが、仕組みを理解しておけば、- 障害調査が圧倒的にスムーズになる
- 高負荷なシステムでも安定運用できる
- パフォーマンスチューニングの精度が上がる
- アプリとインフラの両方を理解できるエンジニアになれる
14. FAQ(よくある質問)
最後に、「java heap space」エラーに関連して読者がよく疑問に持つポイントを具体的で実務に役立つ形で Q&A 形式 にまとめます。 この記事の補完として、検索ユーザーのニーズを広く拾える内容になっています。Q1. java.lang.OutOfMemoryError: Java heap space と
GC overhead limit exceeded はどう違いますか?● java heap space
- ヒープが 物理的に不足した ときに発生する
- 大量データ・コレクション肥大化・設定不足などが原因
● GC overhead limit exceeded
- GC が 一生懸命メモリ回収しているのに、ほぼ回収できていない 状態
- 生存オブジェクトが多すぎて GC が回復不能になったサイン
- メモリリークや参照残りが疑われる
Q2. ヒープをとりあえず増やせば解決しますか?
✔ 一時的には改善することがある
✘ 根本原因の解決にはならない
- データ量に対して純粋にヒープが小さい場合 → 有効
- コレクションやメモリリーク → 再発する
Q3. Javaのヒープはどれくらい増やしてよいですか?
● 上限は「物理メモリの 50〜70%」が一般的
理由:- ネイティブメモリ
- スレッドスタック
- Metaspace
- GCワーカー
- OSのプロセス
Q4. コンテナ(Docker / K8s)で Java が OOMKilled されるのはなぜですか?
● 多くの場合、-Xmx を設定していないことが原因
Docker はコンテナのメモリ制限を Java に伝えないことがあり、Java がホストメモリを参照してヒープを確保しようとする → 制限超過 → OOMKilled。✔ 対策
docker run --memory=1g -e JAVA_OPTS="-Xmx800m"Q5. メモリリークかどうかを簡単に判断する方法はありますか?
✔ 以下を満たす場合、ほぼリークと判断できます:
- アプリの稼働時間とともにヒープ使用量が増加し続ける
- Full GC の後もメモリがほとんど減らない
- Old領域が “階段状” に積み上がっていく
- 数時間〜数日後に OOM が発生する
- 短期的には問題が起きない
Q6. Eclipse / IntelliJ で設定したヒープが反映されません
● よくある原因
- Run Configuration を編集していない
- IDE のデフォルト設定が優先されている
- 他の起動スクリプトの
JAVA_OPTSが上書きしている - プロセス再起動を忘れている
Q7. Spring Boot はメモリを多く使うと聞きましたが本当ですか?
はい。Spring Bootは以下の理由でメモリ消費が増えがちです。- 自動構成(Auto Configuration)
- 多数の Bean 生成
- fat jar のクラスロード
- Webサーバー(Tomcatなど)内蔵
Q8. GC の種類はどれを使うべきですか?
基本的には G1GC を使えば問題ありません。● 用途別の推奨
- Webアプリ → G1GC
- 大量バッチ処理 → Parallel GC
- 超低遅延が必要 → ZGC / Shenandoah
Q9. Cloud Run / Lambda などのサーバーレス環境ではどう扱う?
サーバーレス環境は自動スケールでメモリ制限がタイトなため、ヒープサイズは必ず明示設定 すべきです。 例(Java 11)-XX:MaxRAMPercentage=70また、Cold Start 時にメモリが急増するケースがあるため、ヒープ設定には余裕を持つことが重要です。

