Javaにおける 'java.lang.OutOfMemoryError: GC overhead limit exceeded' の解決方法

intermediate Java2026-05-02| Java JDK 8, 11, 17, 21。高スループットのSpring Bootアプリケーション、Tomcatサーバー、およびLinuxやWindows上のレガシーなバッチ処理で頻繁に発生します。

Error Message

java.lang.OutOfMemoryError: GC overhead limit exceeded
#java#jvm#メモリリーク#ガベージコレクション#パフォーマンスチューニング#eclipse-mat

JVMがガベージコレクションで立ち往生する場合最近、大規模なデータ移行タスク中にこの問題に直面しました。最初の10分間、ログは完璧に見えました。しかし、その後スループットが急落し、CPUファンが鳴り響き、プロセスは最終的に聞き覚えのある、イライラさせるメッセージとともに崩壊しました。

java.lang.OutOfMemoryError: GC overhead limit exceeded

これは典型的な「Heap Space」エラーではありません。JVMが降参を宣言している状態です。ガベージコレクター(GC)が長時間稼働しているにもかかわらず、ほとんど成果が得られず、アプリケーションのリソースが枯渇しているときに発生します。

98/2 ルールデフォルトでは、JVMは実行時間の**98%以上をガベージコレクションに費やし、ヒープの2%**未満しか回収できなかった場合にこのエラーをスローします。これは重要な防御メカニズムです。この制限がないと、CPUが100%に張り付いたまま、わずか数バイトを解放するためにアプリケーションが永久にハングアップしてしまいます。

即効性のある応急処置:ヒープの拡張1GBしか割り当てていないのに2GBのXMLファイルを処理するなど、アプリケーションが通常より大きなデータセットを処理しているだけの場合は、-Xmxパラメータを使用して余裕を持たせることができます。

# 最大ヒープサイズを4GBに引き上げる
java -Xmx4g -jar high-load-app.jar

「パニックボタン」フラグ技術的には、このセーフティチェックを無効にすることも可能です。5時間の移行作業の残り10%を終わらせるために一度だけ使用したことがありますが、細心の注意を払ってください。これを無効にすると、通常はアプリが完全にフリーズするか、その直後に標準的な Java heap space エラーでクラッシュします。

-XX:-UseGCOverheadLimit

根本原因の追求メモリを追加することは、多くの場合、不可避な事態を先延ばしにしているに過ぎません。リークがある場合、16GBのヒープも2GBのヒープと同じ速さで最終的に一杯になります。実際に何がRAMを占有しているのかを確認する必要があります。

1. ヒープダンプでクラッシュ時の状態をキャプチャする推測に頼ってはいけません。失敗した瞬間に状態を保存するようJVMに指示し、後で「犯行現場」を検証できるようにします。

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./diagnostics/crash_dump.hprof

アプリが現在ラグが発生しているが、まだクラッシュしていない場合は、プロセスID(PID)を使用してライブダンプを取得します。

# 最初に 'jps' でPIDを取得する
jmap -dump:live,format=b,file=debug_dump.hprof [PID]

2. 証拠を分析するその .hprof ファイルを Eclipse Memory Analyzer (MAT) または VisualVM に読み込みます。「Leak Suspects」レポートを実行してください。私の経験では、原因は通常、次の4つのいずれかです。

  • Abandoned Collections: 古いエントリを削除しないキャッシュ用の static HashMap。- Resource Leaks: try-with-resources ブロックで囲まれていないデータベースの ResultSet や FileStream。- Object Churn: StringBuilder を使用せずに、ループ内で500,000個の String オブジェクトを作成している。- Missing Pagination: データベースから200,000行を500件ずつのバッチで処理せず、一度に List<User> に取得している。### 3. 実践的なコード修正:ストリーミング vs ロードデータセット全体をRAMにロードするのをやめましょう。Spring Data JPAを使用している場合は、標準のリスト取得をストリームに切り替えます。これにより、ループの実行中にGCが処理済みのオブジェクトをクリーンアップできるようになります。
// 回避:すべてのレコードを一度にメモリに読み込む
List<Transaction> history = repository.findAll(); 
history.forEach(this::calculateTax);

// 推奨:レコードを1つずつ処理する
try (Stream<Transaction> stream = repository.streamAll()) {
    stream.forEach(this::calculateTax);
}

検証:本当に修正されたか?祈るだけで終わらせないでください。アプリの動作中に jstat を使用して、リアルタイムでGCの健康状態を監視します。

# 2秒ごとにGC統計を監視する
jstat -gcutil [PID] 2000

GCT(総GC時間)に注目してください。O(Old Generation)が99%に留まったままGCTが着実に上昇している場合、リークは依然として存在します。健全なアプリケーションは「鋸歯状(ソー・トゥース)」のパターンを示します。つまり、メモリが上昇し、GCが実行され、使用率がきれいなベースラインまで下がります。

Related Error Notes