一段代碼搞崩Java,坑都埋到胸了!
本文轉(zhuǎn)載自微信公眾號(hào)「小姐姐味道」,作者小姐姐養(yǎng)的狗。轉(zhuǎn)載本文請(qǐng)聯(lián)系小姐姐味道公眾號(hào)。
數(shù)字運(yùn)算,是一門(mén)語(yǔ)言安身立命的根本。如果連1+1都變得不可信了,整個(gè)程序就會(huì)變得不可信。
考慮到這樣一段代碼:
- Integer a = 1;
- System.out.println(a);
- Integer b = 2;
- System.out.println( a.intValue() == b.intValue() );
- System.out.println(a.equals(b));
執(zhí)行的結(jié)果,竟然是:
- -996
- true
- true
這時(shí)候,你還敢繼續(xù)把代碼寫(xiě)下去么?
為什么會(huì)這樣?
很簡(jiǎn)單,我們使用反射改變了某些東西。
下面這段代碼,將會(huì)改變一些基本運(yùn)算的執(zhí)行邏輯,理所當(dāng)然屬于埋坑的范疇之一。我們還是先看一下它的行為。
- public class StaticBlock {
- static {
- try {
- Class<?> cls = Integer.class.getDeclaredClasses()[0];
- Field f = cls.getDeclaredField("cache");
- f.setAccessible(true);
- Integer[] cache = ((Integer[]) f.get(cls));
- for (int i = 0; i < cache.length; i++) {
- cache[i] = -996;
- }
- } catch (Exception e) {
- e.printStackTrace();
- //silence
- }
- }
- }
程序使用反射,修改了Integer中cache變量中的內(nèi)容,使得里面的數(shù)字,變成了一個(gè)固定的值。我們這里用的是-996,意思是永遠(yuǎn)沒(méi)有996。
你只要想方設(shè)法把這段代碼給觸發(fā)了,Java的Integer包裝類(lèi),就算是廢了。
我們能這么做,關(guān)鍵就在于cache變量上。
數(shù)字緩存
Java 中有 8 種基本類(lèi)型,鑒于 Java 面向?qū)ο蟮奶攸c(diǎn),它們同樣有著對(duì)應(yīng)的 8 個(gè)包裝類(lèi)型,比如 int 和 Integer,包裝類(lèi)型的值可以為 null,很多時(shí)候,它們都能夠相互賦值。
考慮到下面這段小小的代碼,它的運(yùn)算就經(jīng)歷了多次裝箱拆箱。
- public Integer cal() {
- Integer a = 1000;
- int b = a * 10;
- return b;
- }
我們從字節(jié)碼層面看一下。
- public java.lang.Integer read();
- descriptor: ()Ljava/lang/Integer;
- flags: ACC_PUBLIC
- Code:
- stack=2, locals=3, args_size=1
- 0: sipush 1000
- 3: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
- 6: astore_1
- 7: aload_1
- 8: invokevirtual #3 // Method java/lang/Integer.intValue:()I
- 11: bipush 10
- 13: imul
- 14: istore_2
- 15: iload_2
- 16: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
- 19: areturn
可以看到這么簡(jiǎn)單的運(yùn)算,竟然涉及了valueOf、intValue等方法多次,說(shuō)明它的計(jì)算過(guò)程效率是比單純的數(shù)字運(yùn)算要低效的。
其中valueOf方法,用來(lái)將普通數(shù)字包裝成Integer,我們跟蹤到它的方法。
- public static Integer valueOf(int i) {
- if (i >= IntegerCache.low && i <= IntegerCache.high)
- return IntegerCache.cache[i + (-IntegerCache.low)];
- return new Integer(i);
- }
為了增加轉(zhuǎn)化的效率,Integer內(nèi)部,竟然緩存了i和Integer的對(duì)應(yīng)關(guān)系!這樣在下次用的時(shí)候,就能夠直接進(jìn)行定位。cache變量,就是用來(lái)存放這些中間信息的地方。如果我們通過(guò)反射改變了它,Integer就會(huì)有不正常的行為!
更多
IntegerCache,緩存了 low 和 high 之間的 Integer 對(duì)象,可以通過(guò) -XX:AutoBoxCacheMax 來(lái)修改上限。
- String integerCacheHighPropValue =
- sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
有意思的是,Long也有這樣的Cache,但它的上下限是固定的,和Byte、Short是一樣的。
- static final Long cache[] = new Long[-(-128) + 127 + 1];S
Double和Float比較慘,只能直接new一個(gè),做不了這種緩存。
綜合來(lái)看,Integer是比較特殊的。下面這段代碼,即使我們不做反射魔改,它的輸出依然是不確定的。
- Integer n1 = 123;
- Integer n2 = 123;
- Integer n3 = 128;
- Integer n4 = 128;
- System.out.println(n1 == n2);
- System.out.println(n3 == n4);
這是因?yàn)?,正常情況,它會(huì)輸出true,false;而當(dāng)我們使用AutoBoxCacheMax增加了它的上限,它就會(huì)輸出true,true。果然對(duì)象之間相互比較,還是得用equals才相對(duì)靠譜一點(diǎn)啊。
End
看著這個(gè)齊胸小坑,我的感情真的是難以言表。這段代碼整體看來(lái),如果進(jìn)行了正常的review,還是很容易看出問(wèn)題的,但凡是總有萬(wàn)一。
如果這段代碼被放到線上,哪怕是某個(gè)呆萌的同學(xué)不小心練手的時(shí)候提交到了倉(cāng)庫(kù)中,后果都是毀滅性的。這段代碼目的比較直白,但如果我們把cache數(shù)組的修改邏輯,改的復(fù)雜一點(diǎn),在某個(gè)特定的條件下才會(huì)觸發(fā)某單個(gè)變量值的修改,那才是要命的。
畢竟連sonar都掃描不出來(lái),而且jdk中這樣的私有變量,還有一籮筐等著我們?nèi)ヌ剿髂?
作者簡(jiǎn)介:小姐姐味道 (xjjdog),一個(gè)不允許程序員走彎路的公眾號(hào)。聚焦基礎(chǔ)架構(gòu)和Linux。十年架構(gòu),日百億流量,與你探討高并發(fā)世界,給你不一樣的味道。