Java「java heap space」エラー完全解析:原因・対処法・ヒープサイズの最適設定ガイド

目次

1. はじめに

Javaで開発をしていると、ある日突然コンソールに

java.lang.OutOfMemoryError: Java heap space

というメッセージが出て、アプリケーションが落ちてしまう――
そんな経験をしたことはないでしょうか。

このエラーは、「Javaが使えるメモリ(ヒープ領域)が足りなくなった」 という意味です。
しかし、エラーメッセージだけ見ても、

  • 何が原因で足りなくなったのか
  • どこをどう調整すればよいのか
  • コードの問題なのか、設定の問題なのか

といった具体的な判断は、すぐにはできません。
その結果、「とりあえず -Xmx を大きくする」「サーバーのメモリを増やす」といった“場当たり的な対処”に走りがちです。

ですが、原因を理解しないままヒープサイズだけ増やす対処は、根本解決にならないどころか、別のトラブルを招くこともあります。

  • GC(ガーベジコレクション)が重くなりレスポンスが悪化する
  • サーバー全体のメモリが逼迫し、他プロセスに影響が出る
  • 本質的なメモリリークが放置され、再度 OutOfMemoryError が発生する

といった問題に発展する可能性があるためです。

このように、「java heap space」エラーは単なる“メモリ不足”ではなく、
アプリケーション設計・実装・インフラ設定が複合的に絡むトラブルのサイン として捉える必要があります。

1-1. この記事の想定読者

この記事は、次のような方を想定しています。

  • Javaの基本文法(クラス・メソッド・コレクションなど)は理解している
  • しかし、JVMの中でメモリがどう管理されているかまではよく分かっていない
  • 開発・テスト・本番のいずれかで「java heap space」や OutOfMemoryError を経験した、あるいはこれから備えておきたい
  • Docker / コンテナ / クラウド環境で Java を動かしており、メモリ設定まわりに少し不安がある

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

また、記事の後半には 「よくある質問(FAQ)」 の形で、

  • 「とりあえずヒープを増やせばいいのか?」
  • 「どこまでヒープを増やしてよいのか?」
  • 「メモリリークかどうかをざっくり見分ける方法は?」

といった疑問にも答えていきます。

1-3. 記事の読み進め方

「java heap space」エラーは、

  • いますぐ本番障害を解決したい人
  • これからトラブルを未然に防ぎたい人

のどちらにとっても重要なテーマです。

すぐにでも対処が必要な方 は、

  • ヒープサイズの変更方法
  • メモリリークのチェック方法

といった実践的なセクションから先に読んでいただいても構いません。

一方で、時間をかけてきちんと理解したい方 は、

  1. 「Javaヒープとは何か」という基礎
  2. 代表的なエラー原因
  3. その上での解決策・チューニング手順

という順番で読み進めることで、
エラーの背後にある仕組みまでスッキリと整理できるはずです。

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コンパイル結果を保持する領域。

Javaでは、new で生成したオブジェクトはすべてヒープ領域に置かれるという仕組みになっています。

2-2. ヒープ領域の役割

Javaのヒープ領域は、次のようなものを保存する場所です。

  • new で生成されたオブジェクト
  • 配列(List, Map などの中身も含む)
  • ラムダ式が内部で生成するオブジェクト
  • 文字列(String)や StringBuilder のバッファ
  • コレクションフレームワーク内で使用されるデータ構造

つまり、Javaで何かを「記憶」しておく必要がある場合、そのほとんどはヒープに保存されます。

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領域(長生きしたオブジェクト)

GCは領域ごとに異なる方式で動作します。

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 やラムダ式で一時オブジェクトを大量生成

Javaでは、参照が残っている限り GC はオブジェクトを消せません。
つまり、開発者が意図せず参照を保持しているケースが多く存在します。

■ 対応の方向性

  • キャッシュは ライフサイクルを決める
  • コレクションは 容量制限(上限) を決める
  • 大量データを扱う仕組みは 定期的にクリアする

参考までに、メモリリークには見えなくても

List<String> list = new ArrayList<>();
for (...) {
    list.add(heavyData);  // ← ここで永遠に増加
}

このようなコードは非常に危険です。

3-3. メモリリーク(意図しないオブジェクト保持)

JavaはGCがあるため「メモリリークとは無縁」と思われがちですが、
実際には Java でも普通にメモリリークは発生します。

■ よくあるリークの発生ポイント

  • 静的変数(static)にオブジェクトを保持したままにする
  • Listener や Callback の登録解除忘れ
  • Stream / Lambda 内で参照を残し続ける
  • 長期稼働バッチでオブジェクトが蓄積
  • ThreadLocal に大量のデータを入れ、スレッドが再利用されてしまう

Javaの世界でも、メモリリークは完全に避けられるものではありません。

■ 対応の方向性

  • 静的変数の使い方を見直す
  • 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

という 「遅延発生型」 heap space エラー が多発します。

3-6. コンテナ(Docker / Kubernetes)での制限誤解

Docker や Kubernetes でよく発生する落とし穴があります。

■ 落とし穴

  • -Xmx を設定しない
    → Java はコンテナではなくホストの物理メモリ量を参照
    → 使いすぎ → OOM Killer により強制終了

これは 非常に多い本番障害 の1つです。

■ 対応

  • -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() … その中で利用可能な領域

Webアプリや長期稼働プロセスでは、この値をログに出すことで障害調査に役立ちます。

4-3. VisualVM や Mission Control などのツールで確認する

GUI を使って視覚的にヒープを見る方法もあります。

■ VisualVM

  • ヒープ利用量のリアルタイム表示
  • GC実行のタイミング
  • ヒープダンプの取得

Java開発では頻繁に使われる定番ツールです。

■ Java Mission Control(JMC)

  • より詳細なプロファイリングが可能
  • 特に Java 11 以降の運用で便利

ツールを使うと、Old世代だけ増えている といった問題の可視化に役立ちます。

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.jar

5-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.inijetty.conf に下記を追加:

--exec
-Xms512m
-Xmx2048m

Webアプリはトラフィック量に応じてメモリ利用が急増するため、
本番は テストより余裕を持たせる のが基本です。

5-3. Spring Boot アプリの設定

Spring Boot を fat-jar で動かす場合も、基本は同じです。

java -Xms1g -Xmx2g -jar spring-app.jar

Spring 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 実行環境では、メモリ量に応じて次のように設定するのが一般的です。

メモリ総量推奨ヒープ(目安)
1GB512〜800MB
2GB1.2〜1.6GB
4GB2〜3GB
8GB4〜6GB

※ 残りのメモリは OS や GC・スレッドスタック用として確保するため余裕を残す。

クラウド環境は物理メモリが少ないケースも多く、
無計画にヒープを大きくしてしまうとアプリ全体が不安定になります。

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アプリ・業務システムに最適

→ 一般的には G1GC を使えば安心

● ② 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.jar

GC方式によって、ヒープ利用状況や停止時間が大きく変わるため、
本番システムでは明示的に設定することも多いです。

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 発生

これは “GCが頑張っているが、回収しきれずに限界に到達した” 状態です。

■ 主な原因

  • メモリリーク
  • 永久的に保持されたコレクション
  • オブジェクトの寿命が長すぎる
  • Old領域の肥大化

このようなケースでは、GCログを分析することで
「リークの兆候」や「特定タイミングでの負荷集中」などを特定できます。

7-6. G1GC をチューニングするときのポイント

G1GCは優秀ですが、チューニングでさらに安定させられます。

● 代表的なパラメータ

-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=45
  • MaxGCPauseMillis
    → 停止時間の目標値(200msなど)
  • G1HeapRegionSize
    → G1が使うヒープ分割のサイズ
  • InitiatingHeapOccupancyPercent
    → Old領域が何%になったらGCを開始するか

ただし、多くのケースでは デフォルトで問題ない ため、
必要なときだけ変更すれば十分です。

7-7. GCチューニングのまとめ

GCの改善は、ヒープサイズを増やすだけでは分からない

  • オブジェクトの寿命
  • コレクションの扱い
  • メモリリークの有無
  • ヒープの逼迫ポイント

を可視化してくれるため、
「java heap space」対策として非常に重要なプロセスです。

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(疑いのある箇所)を自動表示

● 基本的な解析手順

  1. ヒープダンプファイル(*.hprof)を開く
  2. 「Leak Suspects Report」を実行する
  3. 大量にメモリを保持するコレクションを探す
  4. Dominator Tree を確認し“親オブジェクト”を特定する
  5. 参照パス(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の -Xmx800MB ~ 900MB程度 に抑えるのが安全です。

● なぜリミットより小さく設定するのか?

理由は、Javaが使用するのはヒープだけではないためです。

  • ネイティブメモリ
  • スレッドスタック(数百KB × スレッド数)
  • Metaspace
  • GC ワーカー
  • JITコンパイルコード
  • ライブラリ読み込み

これらも合計すると100〜300MBは軽く使います。

つまり、

「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 が多発するのか?(実例)

本番でよくあるパターン:

  1. K8sで memory limit = 1GB
  2. Java の -Xmx を設定していない
  3. Javaはホストのメモリを参照し、ヒープを 1GB以上確保しようとする
  4. OSがコンテナを強制終了 → OOMKilled

これは java heap space (OutOfMemoryError) ではなく、
コンテナ側の OOM(Out Of Memory) で落ちる点に注意が必要です。

9-7. GCログやメトリクスでのコンテナ特有のチェックポイント

コンテナ環境では以下を重点的に確認します。

  • Pod 再起動が増えていないか
  • OOMKilled のイベントが記録されていないか
  • Old領域が増え続けていないか
  • GC の回収量が極端に少ないタイミングがないか
  • そもそもヒープ以外のネイティブメモリが不足していないか

Prometheus + Grafana を導入しておくと視覚化しやすくなります。

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 設計
  • キャッシュ暴走
  • 中間オブジェクトの大量発生

これらを避けるだけでも、
「java heap space」エラーは大幅に減らすことができます。

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 を使うときはデータ量を意識する

Stream は便利ですが、
“メモリに優しいとは限らない” ことを理解しておくべきです。

12-5. 巨大ファイル・巨大データの扱いはストリーミングに切り替える

大量データの一括処理は、ヒープ問題の根源です。

● ベストプラクティス

  • CSV → Files.lines()
  • JSON → Jackson Streaming
  • DB → ページング
  • API → 分割取得(cursor/pagination)

“メモリにすべて載せない” を徹底するだけで多くの heap space 問題が消えます。

12-6. ThreadLocal を慎重に扱う

ThreadLocal は非常に強力ですが、誤用すると致命的なメモリリークを引き起こします。

● ベストプラクティス

  • スレッドプールと併用する場合は特に注意
  • 値を使い終わったら remove()
  • 長寿命データを入れない
  • static ThreadLocal は極力避ける

12-7. メモリリーク対策としてヒープダンプを定期的に取る

長期稼働するシステム(Webアプリ / バッチ / IoT)では、定期的にヒープダンプを取得して比較するとリークの初期兆候がつかめます。

● 手段

  • VisualVM
  • jmap
  • -XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof

OOM 発生時の自動ダンプは必須設定です。

12-8. GC は必要最小限のチューニングに留める

「GCをいじれば性能が上がるだろう」という誤解は危険です。

● ベストプラクティス

  • まずは デフォルト設定で運用
  • 問題が生じたときに最小限の調整
  • G1GC を基本選択
  • ヒープを増やすほうが有効なことも多い

12-9. そもそもアーキテクチャを分割するという選択肢

データが巨大になりすぎたり、アプリがモノリシックすぎてヒープを大量に必要とする場合、

  • マイクロサービス化
  • データ処理バッチを分割
  • メッセージキュー(Kafka等)で分離
  • 分散処理(Spark等)

という設計改善が必要になるケースもあります。

“ヒープを増やしても増やしても足りない” なら、アーキテクチャ課題を疑うべきです。

12-10. まとめ:Javaのメモリ管理は「積み重ねの最適化」が重要

Javaの heap space 問題は、一つの設定や一つの修正だけで解決することは稀です。

● 覚えておくべき要点

  • ヒープ設定は必ず明示する
  • 監視が最も重要
  • コレクション肥大化を許さない
  • 大量データはストリーミング
  • キャッシュは管理する
  • ThreadLocal は慎重に
  • 必要ならツールでリーク解析
  • コンテナ環境は別ルールで考える

これらのポイントを守ることで、「java heap space」エラーはほぼ確実に防止できます。

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のメモリ管理は難しいように思えるかもしれませんが、仕組みを理解しておけば、

  • 障害調査が圧倒的にスムーズになる
  • 高負荷なシステムでも安定運用できる
  • パフォーマンスチューニングの精度が上がる
  • アプリとインフラの両方を理解できるエンジニアになれる

といったメリットがあります。

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 が回復不能になったサイン
  • メモリリークや参照残りが疑われる

heap space は限界突破、GC overhead は限界直前 という違いがあります。

Q2. ヒープをとりあえず増やせば解決しますか?

✔ 一時的には改善することがある

✘ 根本原因の解決にはならない

  • データ量に対して純粋にヒープが小さい場合 → 有効
  • コレクションやメモリリーク → 再発する

根本原因がリークの場合、ヒープを倍にしてもまた OOM が発生します。

Q3. Javaのヒープはどれくらい増やしてよいですか?

● 上限は「物理メモリの 50〜70%」が一般的

理由:

  • ネイティブメモリ
  • スレッドスタック
  • Metaspace
  • GCワーカー
  • OSのプロセス

これらを考慮し、ヒープ専有は避けるべきです。

特に Docker / K8s では、limit の 70〜80% を -Xmx にするのが実務標準 です。

Q4. コンテナ(Docker / K8s)で Java が OOMKilled されるのはなぜですか?

● 多くの場合、-Xmx を設定していないことが原因

Docker はコンテナのメモリ制限を Java に伝えないことがあり、Java がホストメモリを参照してヒープを確保しようとする → 制限超過 → OOMKilled。

✔ 対策

docker run --memory=1g -e JAVA_OPTS="-Xmx800m"

Q5. メモリリークかどうかを簡単に判断する方法はありますか?

✔ 以下を満たす場合、ほぼリークと判断できます:

  • アプリの稼働時間とともにヒープ使用量が増加し続ける
  • Full GC の後もメモリがほとんど減らない
  • Old領域が “階段状” に積み上がっていく
  • 数時間〜数日後に OOM が発生する
  • 短期的には問題が起きない

ただし、最終的には ヒープダンプ分析(Eclipse MAT) が必要です。

Q6. Eclipse / IntelliJ で設定したヒープが反映されません

● よくある原因

  • Run Configuration を編集していない
  • IDE のデフォルト設定が優先されている
  • 他の起動スクリプトの JAVA_OPTS が上書きしている
  • プロセス再起動を忘れている

IDEごとに VM オプションの設定が異なるため、Run/Debug Configuration の「VM options」欄を必ず確認 してください。

Q7. Spring Boot はメモリを多く使うと聞きましたが本当ですか?

はい。Spring Bootは以下の理由でメモリ消費が増えがちです。

  • 自動構成(Auto Configuration)
  • 多数の Bean 生成
  • fat jar のクラスロード
  • Webサーバー(Tomcatなど)内蔵

そのため、普通の Java プログラムより 200〜300MB ほど多くメモリを使う ケースがあります。

Q8. GC の種類はどれを使うべきですか?

基本的には G1GC を使えば問題ありません。

● 用途別の推奨

  • Webアプリ → G1GC
  • 大量バッチ処理 → Parallel GC
  • 超低遅延が必要 → ZGC / Shenandoah

明確な理由がない限り、G1GC を選択するのが安全です。

Q9. Cloud Run / Lambda などのサーバーレス環境ではどう扱う?

サーバーレス環境は自動スケールでメモリ制限がタイトなため、ヒープサイズは必ず明示設定 すべきです。

例(Java 11)

-XX:MaxRAMPercentage=70

また、Cold Start 時にメモリが急増するケースがあるため、ヒープ設定には余裕を持つことが重要です。

Q10. Java のヒープ不足はどうすれば再発防止できますか?

以下の3点を徹底することで、再発確率は劇的に下がります。

✔ ヒープ設定を明示する

✔ 大量データはストリーミングで処理する

✔ 定期的に GCログとヒープダンプを確認する

これだけで多くの本番障害が回避できます。

まとめ:FAQで疑問を解消しながら、実務のメモリ対策へ

このFAQでは、java heap space エラーについて読者が検索しやすい疑問を取り上げ、実務的な回答をまとめました。

記事本体と合わせて活用することで、「Javaのメモリ問題」に強くなり、アプリケーションの安定運用に大きく役立つはずです。