はじめに (対象読者・この記事でわかること)
この記事は、Javaアプリケーションの稼働時に出力されるGCログを読み解きたいエンジニアや、サービス運用中に「Full GC」が頻発して困っているSRE・インフラエンジニアを対象にしています。
記事を読むことで、GCログに記載される「Full GC」と「Full GC(System)」の呼び分けがなぜ存在するのか、それぞれがどのようなトリガーで発生し、どこまでチューニングで抑制できるのかが一目でわかるようになります。
私が初めて本番ログを見たとき、「(System)」が付く付かないで検索しても明確な説明が見つからず、JVMソースまで読むハメになった経験があるため、同じ無駄をしたくない方の参考になればと思い執筆しました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - Java のヒープ領域(Young / Old / Metaspace)の基礎 - GC ログの出力オプション(-Xlog:gc* または -XX:+PrintGCDetails)の有効化方法 - プロセスのシグナル(SIGTERM や jmap/jcmd)を送った経験
Full GC と Full GC (System) がなぜ区別されるのか
Java の世界では「Full GC」と聞くと Old 領域の掃除を連想しますが、ログを見ると同じ Full でも末尾に "(System)" と付くものが混在していることに気づきます。
これは単なる表記の違いではなく、「誰がトリガーしたのか」 を明示するための区別です。
HotSpot VM 内部では「System.gc()」「jmap -histo」「JVMTI の ForceGarbageCollection」など、明示的な GC 要請が飞来した場合に only なコードパスが存在し、ここでだけ特別なフラグ(_gc_cause == GCCause::_java_lang_system_gc など)を立てて実行されます。
結果的に同じ Serial / Parallel / G1 のアルゴリズムでも、ログ出力時に gclog.cpp の以下分岐により括弧付きで表示されるのです。
Cpp// 簡略化した hotspot/src/share/vm/gc/shared/gclog.cpp if (cause == GCCause::_java_lang_system_gc || cause == GCCause::_jvmti_force_gc) { st->print("Full GC (System)"); } else { st->print("Full GC"); }
ログで見分ける方法と抑制テクニック
Step1: ログの読み方と見分けポイント
まずは実際のログを見てみましょう。JDK 17 + G1GC の例です。
[2025-06-25T12:34:56.789+0900] GC(150) Pause Full (System) 2048M->1024M(4096M) 1.234s
[2025-06-25T12:35:10.123+0900] GC(151) Pause Full 2048M->1024M(4096M) 1.123s
一行目は (System) が付いているため、アプリケーションコードまたは運用ツールが System.gc() を呼び出したことを示します。
二行目は括弧がないため、JVM が Old 領域の空き不足や MetaSpace の拡張失敗などを理由に自律的に Full GC を実行したことを示します。
Step2: それぞれの発生トリガーを抑える JVM オプション
明示的な要請を無視したい場合は以下のフラグが有効です。
-
-XX:+DisableExplicitGC
System.gc()およびRuntime.gc()の呼び出しを無視します。
ただし NIO のDirectByteBuffer解放に依存するコードがあると、Off-Heap OOM を引き起こす可能性があるため注意が必要です。 -
-XX:+ExplicitGCInvokesConcurrent
G1 や ParallelGC では「System.gc() が来たら並行 GC(Young GC + Concurrent Cycle)に昇格させる」オプションです。
ログ上はPause Full (System)ではなくPause Young (Concurrent Start by System GC)のように変化します。 -
メタスペース関連
Full GC (Metadata GC Threshold)というログは JVM が自律的にメタスペースを拡張できずに発生します。
初期値を大きくしてやる(-XX:MetaspaceSize=256mなど)と頻度を減らせます。
ハマった点:DisableExplicitGC 適用後に DirectBuffer OOM が増えた
私は過去、Tomcat で -XX:+DisableExplicitGC を付けたら数日後に java.lang.OutOfMemoryError: Direct buffer memory が多発し、サービスが落ちる事象に見舞われました。
当時の私は「System.gc() が来ると止まるだけ」と理解していましたが、内部で sun.misc.VM.saveAndRemoveDirectBuffer() → System.gc() の呼び出しが行われており、GC が抑制されると DirectBuffer のクリーンアップが永遠に起きなくなっていたのです。
解決策
以下のように段階的に対応し、無事に運用を安定させました。
-XX:+DisableExplicitGCを外す- 代わりに
-XX:+ExplicitGCInvokesConcurrentを付けて、System.gc を並行 GC に誘導 - ソースコード側で
System.gc()を呼ばない設計にリファクタリング(try-with-resources で即座にクリーンアップ)
まとめ
本記事では、Java の GC ログに記録される「Full GC」と「Full GC (System)」の違いを HotSpot VM のソースとログ例を交えて解説しました。
- 「Full GC (System)」は明示的な GC 要請(System.gc() など)由来である
- 「Full GC」は JVM が自律的に Old 領域やメタスペースの逼迫を検知して実行する
- DisableExplicitGC で抑制できるが、DirectBuffer OOM のリスクがある
- ExplicitGCInvokesConcurrent を使うと、System.gc を並行 GC に昇格させることができる
この記事を通して、ログを見ただけで「誰が GC を起動したのか」が判別できるようになり、チューニングの選択肢が広がったはずです。
次回は「G1 の Mixed GC がなぜフル GC に fallback するのか」について深掘りする予定です。
参考資料
- OpenJDK 17 HotSpot ソース:src/hotspot/share/gc/shared/gclog.cpp
- Oracle 公式:Java 17 GC チューニングガイド
- OpenJDK Wiki: DisableExplicitGC と DirectByteBuffer
- 田中&渡辋『Java パフォーマンス チューニング 実践入門』(技術評論社, 2022)
