概要

ガベージコレクション(GC)は Java が自動で行ってくれるメモリ管理ですが、その動作原理を把握していないと、本番環境での停止時間増大やメモリ不足に対処できません。とくに業務バッチや API サーバーでは「なぜ急に遅くなったのか」「GC ログのどこを見ればいいのか」という問い合わせが現場で繰り返し発生します。この記事では、Young Generation と Old Generation の世代構造、Minor GC と Full GC の違い、Runtime API を使ったメモリ状態の取得方法を扱います。finalize() が非推奨になった経緯と Cleaner への移行についても触れるので、レガシーコードの保守にも役立ちます。

使いどころ

夜間バッチで処理時間が徐々に伸びている原因を GC ログから切り分ける

API サーバーのレスポンスタイムが不安定なとき、Full GC の発生頻度を確認する

レガシーコードの finalize() を Cleaner または try-with-resources に移行する

コード例

GcBasicDemo.java
import java.lang.ref.Cleaner;

public class GcBasicDemo {

    // メモリ状態を record で表現(Java 17+)
    record MemoryStatus(String label, long usedMb, long freeMb,
                        long totalMb, long maxMb) {
        void print() {
            System.out.println("--- " + label + " ---");
            System.out.println("  使用中: " + usedMb + " MB");
            System.out.println("  空き:   " + freeMb + " MB");
            System.out.println("  合計:   " + totalMb + " MB");
            System.out.println("  最大:   " + maxMb + " MB");
        }
    }

    /** Runtime からメモリ状態を取得 */
    static MemoryStatus captureMemory(String label) {
        var rt = Runtime.getRuntime();
        var total = rt.totalMemory();
        var free = rt.freeMemory();
        return new MemoryStatus(label,
            (total - free) / (1024 * 1024),
            free / (1024 * 1024),
            total / (1024 * 1024),
            rt.maxMemory() / (1024 * 1024));
    }

    public static void main(String[] args) throws InterruptedException {
        captureMemory("初期状態").print();

        // 短命オブジェクトを大量生成(Young Generation に配置される)
        System.out.println("\n短命オブジェクトを大量生成...");
        for (int i = 0; i < 10_000; i++) {
            var temp = new String("一時データ " + i);
        }

        captureMemory("短命オブジェクト生成後").print();

        // GC 要求(動作確認用。本番コードでは使わない)
        System.gc();
        Thread.sleep(100);

        captureMemory("GC 後").print();

        // GC 世代構造の説明
        var explain = """
            === GC 世代構造 ===
            Young Generation : 新しいオブジェクト置き場。Minor GC で頻繁に回収。
            Old Generation   : 長生きしたオブジェクト。Full GC でまとめて回収。
            Minor GC : Young のみ対象(短時間・低コスト)
            Full GC  : Old を含む全 GC(長時間・アプリ一時停止)
            """;
        System.out.println(explain);

        // Cleaner の使用例(finalize() の代替)
        var cleaner = Cleaner.create();
        var resource = new Object();
        cleaner.register(resource, () ->
            System.out.println("Cleaner: リソースが GC されました"));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

デフォルト GC は G1GC(Java 9 から)。GC ログは -Xlog:gc* に統一。record でメモリ状態を不変オブジェクトとして扱え、Cleaner が finalize() の代替として定着。

Java 17
// Java 17: record でメモリ状態を不変オブジェクトとして管理
record MemoryStatus(String label, long usedMb, long freeMb, long totalMb, long maxMb) {
    void print() {
        System.out.println("--- " + label + " ---");
        System.out.println("  使用中: " + usedMb + " MB");
    }
}
var runtime = Runtime.getRuntime();
var status = new MemoryStatus("現在",
    (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024),
    runtime.freeMemory() / (1024 * 1024),
    runtime.totalMemory() / (1024 * 1024),
    runtime.maxMemory() / (1024 * 1024));

Library Comparison

標準 API(Runtime / ManagementFactory)ヒープ使用量や GC 回数を簡易的に確認したいとき。外部依存なしで即座に導入できる。ヒープダンプ解析や詳細なオブジェクトプロファイリングには向かない。
VisualVM / JConsoleGUI でリアルタイムにヒープ・GC・スレッドの状況を監視したいとき。JDK に同梱されている。本番環境への JMX 接続にはネットワーク設定が必要。リモート接続にはセキュリティ上の配慮が要る。
Eclipse MATOutOfMemoryError 発生時のヒープダンプを解析し、リーク箇所を特定したいとき。事後解析ツールのため、リアルタイム監視には使えない。ダンプファイルが大きいとツール側のメモリも消費する。

注意点

System.gc() は GC の実行を保証しない。あくまで JVM への要求であり、本番コードに入れるべきではない

finalize() は Java 9 で非推奨、Java 18 で削除対象になった。リソース解放には try-with-resources か Cleaner を使う

Young Generation のサイズが小さすぎると Minor GC が頻発し、オブジェクトが早期に Old Generation へ昇格して Full GC を招く

GC ログの出力形式は Java 8 以前(-XX:+PrintGCDetails)と Java 9 以降(-Xlog:gc*)で異なる。バージョンに応じたオプションを確認すること

FAQ

System.gc() を呼べば確実にメモリは解放されますか。

いいえ。System.gc() は JVM に GC を要求するだけで、実行は保証されません。タイミングも JVM 実装に依存するため、本番コードに入れると逆に性能を落とすことがあります。

Minor GC と Full GC はどう見分けますか。

GC ログで確認します。Minor GC は Young Generation のみ対象で数ミリ秒、Full GC は Old Generation を含む全領域対象で数百ミリ秒以上かかることが多いです。-Xlog:gc* で出力を有効にしてください。

finalize() の代わりに何を使えばよいですか。

ファイルやソケットなど外部リソースには try-with-resources(AutoCloseable)を使います。ネイティブメモリの解放など特殊な用途では java.lang.ref.Cleaner が代替になります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます