概要
Javaのオブジェクト型のキャッシュについて調べてみました。Javaが効率的にメモリを利用するためにどのように動いているか把握できると思います。
Integer型
Integer型は-128~127までの値はキャッシュされ、オブジェクトを使いまわしています。
jshellを用いて確認してみます。
jshell> Integer a = 127;
a ==> 127
jshell> Integer b = 127;
b ==> 127
jshell> System.out.println(a == b); <- オブジェクトが一緒
true
jshell> Integer c = 128;
c ==> 128
jshell> Integer d = 128;
d ==> 128
jshell> System.out.println(c == d); <- オブジェクトが異なる
false
jshell> System.out.println(c.equals(d)); <- オブジェクトが異なるのでequals()メソッドでの比較が必要
true
jshell> Integer e = new Integer(127);
e ==> 127
jshell> Integer f = new Integer(127);
f ==> 127
jshell> System.out.println(e == f); <- new Integer()の場合は別のオブジェクトになることに注意
false
「==」演算子は参照の比較を行うため、両者が同じオブジェクトを参照していることが確認できましたね。注意点としては、new Integer()の場合は別のオブジェクトになることです。
次にopenjdkのコードでIntegerクラスの内部実装を見てみましょう。
IntegerCache()メソッドとvalueOf()、内部クラスのIntegerCacheを下に抜粋しました。実装は見ればすぐに内部クラスのIntegerCache.cache[]で-128~127の値がキャッシュされていることがわかりますね。
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* jdk.internal.misc.VM class.
*
* WARNING: The cache is archived with CDS and reloaded from the shared
* archive at runtime. The archived cache (Integer[]) and Integer objects
* reside in the closed archive heap regions. Care should be taken when
* changing the implementation and the cache array should not be assigned
* with new Integer object(s) after initialization.
*/
private static final class IntegerCache {
static final int low = -128;
static final int high;
@Stable
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
// If archive has Integer cache, we must use all instances from it.
// Otherwise, the identity checks between archived Integers and
// runtime-cached Integers would fail.
int archivedSize = (archivedCache == null) ? 0 : archivedCache.length;
for (int i = 0; i < archivedSize; i++) {
c[i] = archivedCache[i];
assert j == archivedCache[i];
j++;
}
// Fill the rest of the cache.
for (int i = archivedSize; i < size; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
@IntrinsicCandidate
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* jdk.internal.misc.VM class.
*
* WARNING: The cache is archived with CDS and reloaded from the shared
* archive at runtime. The archived cache (Integer[]) and Integer objects
* reside in the closed archive heap regions. Care should be taken when
* changing the implementation and the cache array should not be assigned
* with new Integer object(s) after initialization.
*/
private static final class IntegerCache {
static final int low = -128;
static final int high;
@Stable
static final Integer[] cache;
static Integer[] archivedCache;
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
h = Math.max(parseInt(integerCacheHighPropValue), 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
// Load IntegerCache.archivedCache from archive, if possible
CDS.initializeFromArchive(IntegerCache.class);
int size = (high - low) + 1;
// Use the archived cache if it exists and is large enough
if (archivedCache == null || size > archivedCache.length) {
Integer[] c = new Integer[size];
int j = low;
// If archive has Integer cache, we must use all instances from it.
// Otherwise, the identity checks between archived Integers and
// runtime-cached Integers would fail.
int archivedSize = (archivedCache == null) ? 0 : archivedCache.length;
for (int i = 0; i < archivedSize; i++) {
c[i] = archivedCache[i];
assert j == archivedCache[i];
j++;
}
// Fill the rest of the cache.
for (int i = archivedSize; i < size; i++) {
c[i] = new Integer(j++);
}
archivedCache = c;
}
cache = archivedCache;
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
ちなみに「Integer a = 127;」と「Integer a = Integer.valueOf(127);」は同等として扱われるようですが、なぜでしょうか?
それは、オートボクシングという仕組みが働き、プリミティブ型の値が自動的にラッパークラスのインスタンスに変換されます。その際、Javaコンパイラが「Integer a = Integer.valueOf(127);」に変換しているのです。
その他の型は?
Long型、Short型、Byte型、Character型でも同様にキャッシュしています。
ちなみにLong型、Short型、Byte型は-128~127、Character型は\u0000(0)から \u007F(127)です。
Stringは?
それではString型はどうでしょうか?
"-128" ~ "127"までキャッシュしているわけではなく、文字列リテラルはインターン(interning)というメモリ効率化の機構が用いられています。
文字列リテラルとは、""で囲まれた文字列です。("Hello"など)文字列リテラルが作成されると、文字列プール(String Pool)といる領域に格納されます。
文字列リテラルがプログラム内で複数回使用される際に、同じ文字列リテラルは一度だけメモリ上に格納され、その後はその既存の文字列オブジェクトが再利用されます。この仕組みにより、メモリの使用量が減り、文字列の比較操作が高速化されます。
String m = "hello";
String n = "hello";
System.out.println(m == n); // true (同じリテラルを参照)
String o = new String("hello");
System.out.println(m == o); // false (異なるオブジェクトを参照)
newしたときも同じ文字の場合は効率的に扱いたいよねという話もあると思いますが、そのときはintern()メソッドを使いましょう。
intern()メソッドを使うと、文字列がインターンされ、文字列プール内に既に同じ文字列が存在する場合、その文字列の参照が返されます。存在しない場合は、その文字列がプールに追加されます。
String p= new String("Hello").intern();
System.out.println(n == p); // true
intern()メソッドのメリット・デメリット
メリット
- メモリ使用量が削減され、==演算子を使った比較が高速になります(すべてが文字列プールに収まれば)
- 大量の同じ文字列を扱う場合、パフォーマンスが向上する場合があります
デメリット
- 必要以上に多くの文字列をプールに保存すると、メモリが無駄に消費される可能性があります
- またintern()メソッドを多用すると、プール操作にかかるオーバーヘッドが発生することがあります
まとめ
オブジェクトの型によってキャッシュとインターンという仕組みの違いがあります。比較には==
とequals
を適材適所で利用する良いと思います。