概要

「このバッチ、メモリ足りてるのか」「List に何万件入れたらどのくらい消費するのか」――業務でこうした疑問を持ったとき、まず手軽に確認できるのが Runtime API によるヒープメモリの計測です。外部ツールやプロファイラを持ち出す前に、コード数行で概算を把握できるため、開発中の見積もりや障害時の一次切り分けに重宝します。この記事では、Runtime.getRuntime() から取得できる maxMemory / totalMemory / freeMemory の意味の違い、大量データ生成前後のメモリ変化の計測、GC 実行後の回収量の確認方法を扱います。record を使ったスナップショットの管理パターンも示すので、計測ロジックを使い回しやすくなります。

使いどころ

CSV 取込バッチで 10 万行を List に溜め込んだときのヒープ消費量を概算する

メモリ不足が疑われるとき、処理の前後で使用量を記録して増分を確認する

GC 後にどの程度メモリが回収されたかを確認し、リークの有無をざっくり判断する

コード例

MemoryMeasure.java
import java.util.ArrayList;

public class MemoryMeasure {

    // メモリスナップショットを record で管理(Java 17+)
    record MemorySnapshot(long maxKb, long totalKb, long usedKb, long freeKb) {
        /** 現在のヒープ状態をキャプチャ */
        static MemorySnapshot capture() {
            var rt = Runtime.getRuntime();
            var total = rt.totalMemory();
            var free = rt.freeMemory();
            return new MemorySnapshot(
                rt.maxMemory() / 1024,
                total / 1024,
                (total - free) / 1024,
                free / 1024);
        }

        void print(String label) {
            System.out.println("--- " + label + " ---");
            System.out.printf("最大ヒープ   : %,d KB%n", maxKb);
            System.out.printf("確保済み     : %,d KB%n", totalKb);
            System.out.printf("使用中       : %,d KB%n", usedKb);
            System.out.printf("空き         : %,d KB%n", freeKb);
        }
    }

    /** 大量データ生成とメモリ変化の計測 */
    static void measureLargeList() {
        MemorySnapshot.capture().print("リスト生成前");

        var list = new ArrayList<String>();
        for (int i = 0; i < 100_000; i++) {
            list.add("item-" + i);
        }
        MemorySnapshot.capture().print("リスト生成後(10万件)");

        // 参照を切って GC を要求
        list = null;
        System.gc();
        try { Thread.sleep(100); }
        catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        MemorySnapshot.capture().print("GC 後");
    }

    public static void main(String[] args) {
        System.out.println("=== ヒープメモリ計測 ===");
        measureLargeList();
    }
}

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

Version Coverage

record でスナップショットを不変オブジェクトとして管理できる。ファクトリメソッド(static メソッド)を record 内に定義すると、取得と保持を一箇所にまとめられる。

Java 17
// Java 17: record + ファクトリメソッドでスナップショット管理
record MemorySnapshot(long maxKb, long totalKb, long usedKb, long freeKb) {
    static MemorySnapshot capture() {
        var rt = Runtime.getRuntime();
        return new MemorySnapshot(
            rt.maxMemory() / 1024, rt.totalMemory() / 1024,
            (rt.totalMemory() - rt.freeMemory()) / 1024,
            rt.freeMemory() / 1024);
    }
}

Library Comparison

標準 API(Runtime)ヒープ使用量の概算をコード内で手軽に確認したいとき。外部依存ゼロで即座に使える。ヒープ全体の概算値しか取得できない。オブジェクト単位の消費量やネイティブメモリは計測できない。
JOL(Java Object Layout)個々のオブジェクトがメモリ上でどのくらいのサイズを占めるかを正確に知りたいとき。外部依存の追加が必要。プロダクションコードに組み込むものではなく、調査用途に限定される。
VisualVM / Eclipse MATヒープダンプを取得して、どのクラスがメモリを占有しているかを詳細に分析したいとき。事後解析が中心で、コード内でのリアルタイム計測には向かない。

注意点

Runtime.totalMemory() は「JVM が現在確保しているヒープ」であり、maxMemory() とは異なる。ヒープはアプリケーションの需要に応じて totalMemory が maxMemory まで拡張される

System.gc() は GC の実行を保証しない。計測コードでは便宜的に使うが、本番コードには入れないこと

メモリ計測は GC のタイミングに左右される。同じコードでも実行ごとに数値が変わるため、傾向を見ることが重要

Runtime API で取得できるのはヒープメモリのみ。DirectByteBuffer やネイティブメモリの消費は含まれない。NIO を使う場合は注意

FAQ

maxMemory と totalMemory の違いは何ですか。

maxMemory は JVM が使える上限(-Xmx 相当)、totalMemory は現在確保されているヒープサイズです。アプリの需要に応じて totalMemory は maxMemory まで拡張されます。

メモリ計測の結果が毎回異なるのはなぜですか。

GC のタイミングや JIT コンパイルの状況で使用量が変動します。1回の計測ではなく、複数回の傾向を見ることが重要です。

大量データを扱うときのメモリ見積もりはどうすればよいですか。

小規模データで計測した1件あたりのメモリ消費量を、想定件数に掛けて概算します。Runtime API での before / after 計測がそのまま見積もりの根拠になります。

関連書籍

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

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