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)で -128
~ 127
と定められており、実装によっては起動オプションによって上限を拡張することも可能です。
そのため、この範囲内の値については常に同じインスタンスが返され、==
による参照比較でも true
になります。
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(...)
は非推奨、使わないこと