2014-07-05

java.lang.Integer 的 cache?

前幾天遇到鬼打牆的問題,先用簡單的方式來還原現場。
for (int i = -130; i < 130; i++) {
  System.out.println(i + ">" + (Integer.valueOf(i) == Integer.valueOf(i)));
}
以前不知道所以也不會去用 Integer.valueOf(int) 來產生 Integer 物件,只知道用 new Integer(int),那這兩個 method 有什麼差別嗎?先看看上面程式的執行結果。
-130>false
-129>false
-128>true
-127>true
...
126>true
127>true
128>false
129>false
奇怪了,在 -128 到 127 之間的 Integer 相等(不正常),在這範圍外的 Integer 不相等(這才正常),見鬼了!

Google 之後,得到的結論,java.lang.Integer 用了  cache

先複習一下,用兩個等號來比較物件時,除非是同一個物件,否則就算是同樣的值,也是不相等的。
Integer a = new Integer(100);
Integer b = new Integer(100);
System.out.println("a == b > " + (a == b)); // a == b > false

Integer c = Integer.valueOf(100);
Integer d = Integer.valueOf(100);
System.out.println("c == d > " + (c == d)); // c == d > true
a 與 b 不相等可以理解,為什麼 c 和 d 卻相等呢?

來看看 Source code 吧(好像是打從學 java 第一次看 JDK 的 原始碼)。
public static Integer valueOf(int i) {
 if(i >= -128 && i <= IntegerCache.high)
  return IntegerCache.cache[i + 128];
 else
  return new Integer(i);
}
這是 java.lang.Integer 的 valueOf(int),謎底揭曉,原來為了效能與記憶體空間,JVM 將 -128 到 127 之間的 Integer 在透過 valueOf(int) 產生時,是使用 cache 的 Integer 物件,但是若是用 new Integer(int) 則不會使用 cache 物件。

由於是用 cache 物件,所以在 -128 到 127 之間的 valueOf(int) 都會得到相同的物件,也就因此用兩個等號時會相等。

那這個 cache 範圍可以修改嗎?可以的,只要加上 -XX:AutoBoxCacheMax=<size> 這樣的 VM option 就好了。
再執行一次最上面的測試程式,可以發現 128 和 129 從 false 變成 true 了,不過不幸的是,目前只能改正數的值,不能改負數的值。
-130>false
-129>false
-128>true
-127>true
...
126>true
127>true
128>true
129>true

再往下挖一點,將 -XX:AutoBoxCacheMax=<size> 加到 Tomcat 的 catalina.bat 裡。
...
rem ---------------------------------------------------------------------------

set JAVA_OPTS=%JAVA_OPTS% -XX:AutoBoxCacheMax=150

rem Guess CATALINA_HOME if not defined
...
爆了,Tomcat 起不來,錯誤訊息是 Tomcat 不認識 AutoBoxCacheMax,這這這...又是什麼情況啊!
Unrecognized VM option 'AutoBoxCacheMax=150'
Could not create the Java virtual machine.
這年頭沒有 Google 怎麼寫程式?

以下已經超出我可裡理解的範圍了,起因在於 Java Hotspot,一般預設是 Java Hotspot Client VM,可以從以下兩個指令看出來。
C:\Users\Neil>java -version
java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) Client VM (build 20.45-b01, mixed mode, sharing)

C:\Users\Neil>java -client -version
java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) Client VM (build 20.45-b01, mixed mode, sharing)
Google 說只要改用 Server VM 就可以讓 Tomcat 使用 -XX:AutoBoxCacheMax。

當然 Google 會順便告訴我們怎麼改用 Server VM,開啟 <JDK>/jre/lib/i386/jvm.cfg。
-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
改變 -client 與 -server 的順序後存檔。
-server KNOWN
-client KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
再執行一次 java -version 檢查是否生效。
C:\Users\Neil>java -version
java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) Server VM (build 20.45-b01, mixed mode)
最後再重起 Tomcat 就大功告成了。

-----------------------------
最後說一下為什麼會遇到這個問題,既然我都是用 new Integer(int) 的話,那是誰用了 valueOf(int)?是 Hibernate!

我的 Primary Key 的型別剛好是 Integer,請 Hibernate 從資料庫取回物件時,Hibernate 用了 valueOf(int)。

然後方便的程式壞習慣用太久就會出亂子的我,在某一天不小心用了兩個等號去比較兩個物件 Primary Key 是否相等,我以為 Java 會替我 Unboxing,但在兩個等號的運算時,得兩邊分別是 primitive type 和 Object,才會使用 unboxing,如果兩邊都是 Object,就不會用 unboxing,而是看 reference。

再加上我的本機測試資料少少的,所以在 -128 到 127 的庇佑下,本機測試沒問題,在正式機上就沒這麼好講話了。
---
---
---

沒有留言:

張貼留言