🐡

Java の Integer キャッシュを理解する

に公開

Java の Integer には キャッシュ機構 があり、思わぬ挙動を示すことがあります。
今回は IntegerCache の仕組みと、その効果をまとめます。

Integer の動作確認

以下のコードを実行すると、出力はコメントの通りになります。

Integer a = 127;
Integer b = 127;
System.out.println(a == b);      // true

Integer x = 128;
Integer y = 128;
System.out.println(x == y);      // false

System.out.println(a.equals(b)); // true
System.out.println(x.equals(y)); // true

直感的にはすべて true になりそうですが、実際には キャッシュの有無 で動作が変わります。

IntegerCache とは?

Integer には IntegerCache という内部クラスがあり、指定された範囲の Integer オブジェクトをあらかじめ配列に保持しています。
このキャッシュ範囲は JLS(Java Language Specification)で -128127 と定められており、実装によっては起動オプションによって上限を拡張することも可能です。
そのため、この範囲内の値については常に同じインスタンスが返され、== による参照比較でも true になります。

参考: Integer.java (OpenJDK 17)

private static class IntegerCache {
    static final int low = -128; // キャッシュの下限は-128 と固定されている
    static final int high; // キャッシュの上限は後の static 初期化子で決定
    static final Integer[] cache; // 実際に使用するキャッシュ配列
    static Integer[] archivedCache; // CDS で事前に凍結・保存されていた配列を受け取るための一時置き場

    static {
        // 既存の上限は127
        int h = 127;
        // VMが保存している起動プロパティを取得
        String integerCacheHighPropValue =
            VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                // VMが保存している起動プロパティが127未満なら切り上げ
                h = Math.max(parseInt(integerCacheHighPropValue), 127);
                // 配列がオーバーフローしないようにガード
                h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
            }
        }
        high = h;

        // CDSから復元を試みる呼び出し
        CDS.initializeFromArchive(IntegerCache.class);
        // 要求サイズを計算
        int size = (high - low) + 1;

        // アーカイブがない、または要求サイズの方が大きいときは作り直し
        if (archivedCache == null || size > archivedCache.length) {
            Integer[] c = new Integer[size];
            int j = low;
            for(int i = 0; i < c.length; i++) {
                c[i] = new Integer(j++);
            }
            archivedCache = c;
        }
        // cacheに登録
        cache = archivedCache;
        // 最低保証を満たしているかのチェック
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

CDS とは?

CDS (Class Data Sharing) = クラスデータ共有

JVM の 起動を速くし、メモリを節約するための仕組み です。
Java は起動時に多くの標準ライブラリをロードしますが、毎回ゼロから読み込むと時間がかかります。

そこで、よく使うクラスを 事前にアーカイブ(保存) しておき、起動時にそのアーカイブから即座に読み込むことで高速化を実現しています。

IntegerCache の場合も、もし事前にアーカイブされていれば、その内容を利用して 初期化をより速く行える ようになっています。

キャッシュが効く場合・効かない場合

キャッシュが効く(== で true になる可能性あり)

  • オートボクシング
  • Integer.valueOf(int)
  • Integer.valueOf(String)

キャッシュが効かない(毎回新しいインスタンス)

  • new Integer(...)(JDK 9 以降 非推奨
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false

Integer e = new Integer(127);
Integer f = new Integer(127);
System.out.println(e == f); // false

なぜ -128 ~ 127 なのか?

  • 頻出する小さな整数をキャッシュすることで、メモリ節約と性能改善ができる
  • HotSpot では起動オプション -XX:AutoBoxCacheMax=<N> で上限を変更可能(下限は固定 -128)

注意点

  • == を使った比較はバグの温床

    • 範囲内は true、範囲外は false になるため、挙動が一貫しない
    • 値比較は常に equals またはアンボクシングして ==
  • Map<Integer, ...> のキー比較は安全

    • equals/hashCode を使うので問題ない
    • ただし null のアンボクシングには注意(NullPointerException
  • パフォーマンス調整は最後の手段

    • AutoBoxCacheMax を上げるより、まずは ボクシング自体を減らす設計 を優先

ベンチマーク

JMH を使ってキャッシュ有無の性能を比較しました。

@State(Scope.Thread)
public class BoxingBench {
    int v = 42;

    @Benchmark public Integer valueOfCached() { return Integer.valueOf(v); }
    @Benchmark public Integer newInteger()    { return new Integer(v); } // 非推奨
}

実行結果:

Benchmark                   Mode  Cnt  Score   Error   Units
BoxingBench.newInteger     thrpt    5  0.698 ± 0.059  ops/ns
BoxingBench.valueOfCached  thrpt    5  2.170 ± 0.070  ops/ns
  • valueOfCached: キャッシュ配列から返すだけなので高速・安定
  • new Integer: 毎回ヒープに新しいオブジェクトを生成するため遅い。GC の対象にもなる

まとめ

  • Integer には -128 ~ 127 のキャッシュ がある
  • そのため == の結果は値によって変わる
  • 値の比較は必ず equals またはアンボクシングして ==
  • new Integer(...) は非推奨、使わないこと

Discussion