java-recipes

ホーム ガベージコレクション(GC) › GC-01

GC-01: Mark & Sweep・Generational GC の基本

Java の GC(ガベージコレクション)がどのようにメモリを自動管理するかを理解しましょう。 Mark & Sweep の仕組み、Young/Old Generation の役割、そして GC の動作をRuntime クラスで観察する方法を学びます。

いつ使うか

  • アプリが遅い原因として GC の頻発を疑ったとき
  • OutOfMemoryError が発生してメモリリークを調査したいとき
  • バッチ処理や大量データ処理でメモリ使用量の変化を監視したいとき
  • GC ログを読んで Young/Old Generation の状況を把握したいとき

GC の基本概念

領域役割GC の種類
Young Generation新しく作成されたオブジェクト置き場Minor GC(短時間・頻繁)
Old Generation長生きしたオブジェクトの置き場Full GC(長時間・アプリ一時停止)
Metaspaceクラス定義の情報(Java 8+)特定条件下で回収

Mark & Sweep: まず「到達可能なオブジェクト(Mark)」を辿り、到達できないものをゴミとして「削除(Sweep)」するアルゴリズムです。

サンプルコード

GcBasicSample.java
public class GcBasicSample {

    // GC の動作を観察するためのサンプルクラス
    static class HeavyObject {
        private final byte[] data;
        private final int id;

        HeavyObject(int id, int sizeMb) {
            this.id = id;
            this.data = new byte[sizeMb * 1024 * 1024]; // 指定MBのデータ
        }

        @Override
        public String toString() {
            return "HeavyObject#" + id + "(" + data.length / (1024 * 1024) + "MB)";
        }
    }

    // ランタイムのメモリ情報を表示
    static void printMemoryStatus(String label) {
        Runtime runtime = Runtime.getRuntime();
        long totalMemory = runtime.totalMemory();
        long freeMemory = runtime.freeMemory();
        long usedMemory = totalMemory - freeMemory;
        long maxMemory = runtime.maxMemory();

        System.out.println("--- " + label + " ---");
        System.out.println("  使用中: " + (usedMemory / (1024 * 1024)) + " MB");
        System.out.println("  空き:   " + (freeMemory / (1024 * 1024)) + " MB");
        System.out.println("  合計:   " + (totalMemory / (1024 * 1024)) + " MB");
        System.out.println("  最大:   " + (maxMemory / (1024 * 1024)) + " MB");
    }

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

        // 短命オブジェクトの生成(Young Generation に入る)
        System.out.println("\n短命オブジェクトを大量生成...");
        for (int i = 0; i < 1000; i++) {
            // ローカル変数にしか参照がないため、すぐに GC 対象になる
            String temp = new String("一時的な文字列 " + i);
            if (i % 100 == 0) {
                System.out.print(i + " ");
            }
        }
        System.out.println();

        printMemoryStatus("短命オブジェクト生成後");

        // 明示的 GC 要求(推奨しないが、動作確認用)
        System.out.println("\nGC 要求(System.gc())...");
        System.gc();
        Thread.sleep(100); // GC の実行を待つ

        printMemoryStatus("GC 後");

        // try-with-resources を使うことを推奨
        System.out.println("\n推奨: try-with-resources で確実なリソース解放を");
        System.out.println("非推奨: finalize() はデストラクタではない(実行タイミング不定)");
    }
}

Java 8 では Runtime.getRuntime() でメモリ情報を取得します。System.gc() は GC の実行をリクエストしますが、必ず即座に実行されるとは限りません。finalize() はデストラクタとして使ってはいけません(実行タイミングが JVM に依存するため)。

よくあるミス・注意点

System.gc() は本番コードで使わない

System.gc() は GC の実行を JVM にリクエストしますが、 必ず実行される保証はありません。また本番環境では不意な GC 停止を引き起こす可能性があります。 動作確認用途にとどめ、本番コードには含めないようにしましょう。

finalize() をデストラクタとして使ってはいけない

finalize() メソッドは GC 時に呼ばれる可能性がありますが、 呼ばれるタイミングは不定で、呼ばれないこともあります。 リソースの確実な解放には try-with-resources を使いましょう。 Java 9 以降は非推奨、Java 18 以降は削除予定です。

長命なオブジェクトが Old Generation を圧迫する

static フィールドや長期間参照が続くコレクションにオブジェクトを追加し続けると、 Old Generation が増え続けます。定期的なクリアや WeakReference の活用で Old Generation を小さく保ちましょう。

テストする観点

  • Runtime.getRuntime().maxMemory() が JVM 起動オプション -Xmx で設定した値と一致すること
  • 大量のオブジェクト生成後に System.gc() を呼び出すと、使用メモリが減少すること
  • ローカル変数として作成したオブジェクトはメソッド終了後に GC 対象になること(参照が外れること)
  • static フィールドに保持したオブジェクトは GC で回収されないこと(メモリリークの再現)

GitHub でソースコードを見る →