Javaヒープは、JVM(Java仮想マシン)が管理し、newで生成したオブジェクトを格納するメモリ領域です。スレッドごとのスタックやJVM本体が使うネイティブメモリ(Cヒープ)とは役割も管理方法も異なり、ここを取り違えるとOutOfMemoryError: Java heap spaceの原因を見誤ります。本記事はヒープの構造、-Xmxなどのサイズ設定と確認方法、メモリ不足時の対処までを整理します。
まとめ:Javaヒープの要点
- JavaヒープはJVMが管理し全スレッドで共有する、オブジェクト格納用の領域。ガベージコレクション(GC)が自動で解放する。
- スレッドスタックはスレッドごとに独立し、メソッドのフレームとローカル変数を保持する。ヒープとは別領域。
- Cヒープ(ネイティブメモリ)とメタスペースはJavaヒープの外側にあり、
-Xmxの管理下に入らない。 - 最大ヒープは
-Xmx、初期ヒープは-Xmsで指定。未指定時の最大値は物理メモリの約1/4。 OutOfMemoryError: Java heap spaceは「ヒープが足りない/解放されない」サイン。まずリークかサイズ不足かを切り分ける。
Javaヒープとは:JVMのメモリ領域での位置づけ
Javaプログラムのメモリは1つの塊ではなく、用途ごとに分かれています。ヒープ・スタック・ネイティブメモリ・メタスペースの違いを押さえると、どの設定がどこに効くかが分かります。JVM全体の役割はJava仮想マシン(JVM)とは何かをわかりやすく解説で補完できます。
| 領域 | 格納するもの | 共有 | 管理・解放 | 関連フラグ |
|---|---|---|---|---|
| Javaヒープ | オブジェクト・配列 | 全スレッド共有 | GCが自動解放 | -Xms / -Xmx |
| スレッドスタック | フレーム・ローカル変数 | スレッド固有 | メソッド終了で自動破棄 | -Xss |
| メタスペース | クラス情報・メソッド情報 | 共有(ヒープ外) | ネイティブメモリ・クラスアンロード時 | -XX:MaxMetaspaceSize |
| Cヒープ(ネイティブ) | JVM本体・JITコード・スレッド | プロセス全体 | JVM/OSが管理 | (-Xmx対象外) |
ヒープとスタックの違い
ヒープは全スレッドで共有され、オブジェクトの実体を長く保持します。スタックはスレッドごとに独立し、メソッド呼び出しのたびにフレームを積み、メソッドが戻ると即座に破棄されます。ローカル変数がプリミティブ型ならスタック上に、オブジェクトなら参照だけがスタックに載り実体はヒープに置かれる、という分担です。スタックが深い再帰などで枯渇するとStackOverflowError、ヒープが枯渇するとOutOfMemoryErrorと、エラーの種類で原因領域を判別できます。
Cヒープ・メタスペースのヒープ外配置
「c heap」で語られるCヒープは、JVMプロセス自身が動くためのネイティブメモリで、JITコンパイル済みコードやスレッド管理領域が入ります。Java 8でPermGen(永久代)が廃止され、クラスのメタデータはメタスペースへ移りましたが、これもヒープではなくネイティブメモリ上にあります。どちらも-Xmxの管理外なので、ヒープを増やしてもここは増えません。ネイティブ側の使用量はNMTの概要とJava HotSpot VMにおけるメモリ追跡の重要性で解説するNative Memory Tracking(-XX:NativeMemoryTracking)で追えます。
ヒープの世代別構造とガベージコレクション
Javaヒープは、寿命の短いオブジェクトを置く新生代と、長生きしたオブジェクトが移る老年代に分かれます。この世代分割はGCを効率化するための設計です。
新生代・老年代とGCの流れ
新生代はEden領域と2つのSurvivor領域(S0・S1)で構成されます。newされたオブジェクトはまずEdenに置かれ、Minor GCを生き延びるとSurvivor間を移動し、一定回数を超えて残ると老年代へ昇格します。老年代を対象にするMajor GC(しばしばヒープ全体を回収するFull GCを伴う)は頻度こそ低いものの、停止時間が長くなりがちです。GCの仕組みと種類の詳細はJavaのガベージコレクション(GC)とは|仕組み・種類・チューニングの基礎にまとめています。
GCの種類とデフォルト
Java 9以降のデフォルトGCはG1 GCで、ヒープを小さなリージョンに分割して停止時間を抑えます。より低遅延を狙うなら、Java 15で本番利用可能になったZGC(Java 21で世代別ZGCが正式化)を-XX:+UseZGCのように明示します。同系統のShenandoahもありますが、Oracle JDKには同梱されず主にOpenJDK系ビルドで利用できます。数百MB程度の一般的な業務アプリはデフォルトのG1で足り、GCを替える前にまずヒープサイズと後述のリーク有無を確認するのが順序です。
System.gc()による強制解放の可否
結論として、System.gc()はGCを「要求」するだけで、即時実行も完全な解放も保証されません。むしろ意図しないFull GCを誘発して性能を落とすことがあり、本番コードでの呼び出しは避けるべきです。運用側で無効化するなら-XX:+DisableExplicitGCを付けて明示呼び出しを無視させます。メモリが減らないなら、強制ではなく参照を切る(不要なコレクションやキャッシュを保持し続けない)ことが本筋です。
ヒープサイズの設定と確認方法
ヒープ関連の問題の多くは、まず現在値を測ってから設定します。推測でフラグを増やす前に確認コマンドで実測しましょう。
-Xms / -Xmx / -Xss の指定
初期ヒープを-Xms、最大ヒープを-Xmxで指定します。両者を同じ値にすると起動後の拡張に伴うFull GCを避けられます。スレッドスタックは-Xssで、64bit HotSpotの既定は約1MBです。
# 初期1GB・最大2GB、スレッドスタック512KB、OOM時にヒープダンプ出力
java -Xms1g -Xmx2g -Xss512k \
-XX:+HeapDumpOnOutOfMemoryError \
-jar app.jar
コンテナ環境では-Xmx固定より、割り当てメモリに追従する-XX:MaxRAMPercentage=75.0が扱いやすい場面もあります。
現在のヒープ使用量の確認
稼働中プロセスはjcmdが手軽で、jmap -heapは非推奨のためこちらを使います。世代ごとの推移はjstatで追えます。
# プロセスIDを調べてヒープ構成・使用量を表示
jcmd <pid> GC.heap_info
# 1秒間隔でGC統計(各世代の使用率)を監視
jstat -gc <pid> 1000
# 有効な最大ヒープをフラグから確認
java -XX:+PrintFlagsFinal -version | grep -i MaxHeapSize
コード内ならRuntime.getRuntime().maxMemory()で最大ヒープを、totalMemory()とfreeMemory()で現在の確保量と空きをバイト単位で取得できます。
「OutOfMemoryError: Java heap space」の原因と対処
java.lang.OutOfMemoryErrorは末尾のメッセージで原因領域が変わります。まずここを読み分けます。
- Java heap space:Javaヒープが不足。オブジェクトを保持し続けるリークか、単純なサイズ不足。
- Metaspace:クラスのロード過多。
-XX:MaxMetaspaceSizeやクラスローダーのリークを疑う(ヒープ拡張では直らない)。 - GC overhead limit exceeded:GCに時間を使う割に回収できていない。実質ヒープ不足かリークの前兆。
「Java heap space」への対処は、サイズ不足かリークかの切り分けが先です。-XX:+HeapDumpOnOutOfMemoryErrorで出力したダンプ、または稼働中プロセスからjcmd <pid> GC.heap_dump filename=heap.hprof(旧来のjmap -dumpに代わる推奨手順)で取得したダンプをEclipse MATなどで開き、支配的なオブジェクト(Dominator Tree)を確認します。特定のリスト・マップ・キャッシュが単調増加していればリークで、-Xmxを増やしても再発します。増え続ける参照を断つ(不要になった要素の除去、キャッシュのサイズ上限やWeakリファレンス導入)ことが根本対処です。増加が頭打ちで一時的に不足しているだけなら、実測ピークに余裕を足して-Xmxを引き上げます。
よくある質問
ヒープとスタックの違いは何ですか?
ヒープは全スレッドで共有しオブジェクトの実体を保持する領域で、解放はGCが行います。スタックはスレッド固有で、メソッドのフレームとローカル変数を保持し、メソッドが戻ると自動で破棄されます。枯渇時のエラーも異なり、深い再帰などでスタックが尽きるとStackOverflowError(対処は-Xss拡張や再帰の見直し)、ヒープが尽きるとOutOfMemoryErrorになります。
JavaヒープとCヒープはどう違いますか?
JavaヒープはJVMが管理しGCが自動解放する、オブジェクト用の領域です。Cヒープ(ネイティブメモリ)はJVMプロセス自身が動くための領域で、-Xmxの管理外です。ヒープを増やしてもCヒープは増えません。
ヒープサイズを指定しないとどうなりますか?
最大ヒープは既定で物理メモリの約1/4に設定されます。初期ヒープはさらに小さく、負荷に応じて拡張されます。安定運用では-Xmsと-Xmxを明示し、コンテナでは-XX:MaxRAMPercentageの利用も検討します。
System.gc()でメモリを強制解放できますか?
できません。System.gc()はGCを要求するだけで実行や完全解放は保証されず、性能劣化の原因にもなります。解放されないメモリは、強制するのではなく不要な参照を切って回収対象にするのが正攻法です。
「OutOfMemoryError: Java heap space」が出たらまず何をすべきですか?
ヒープダンプを取得し、単調増加しているオブジェクトがないかを確認します。リークならその参照を断ち、単なる不足なら実測ピークに合わせて-Xmxを引き上げます。メッセージが「Metaspace」なら対処領域が変わり、-XX:MaxMetaspaceSizeの見直しやクラスローダーのリーク(動的クラス生成の多用など)を疑います。