tifyty

pure Java, what else ?

Kategória archívok: bad bad bad

Még mindig az Integer cache, de utoljára

A miért billeg cikkben körbejártuk egy kicsit, hogy mi a helyzet az Integer cache-sel. A cikk végén, pedig a szavazásba beleírtam, hogy

A szavazásba bele sem mertem írni, hogy reflection-nel megoldható, hogy az alsó határ se -128 legyen. Még valaki képes lett volna arra szavazni.

Ebben a cikkben azért mégiscsak megnézzük, hogy ha valaki ezt megpróbálná, akkor mire kell felkészülnie, és milyen trükköket kell alkalmaznia. A cikk végén pedig lesz csattanó is.

Mit is szeretnénk?

Azt szeretnénk elérni, hogy az Integer autoboxing ne csak -128 és 127 közötti értékekre adjon konstand Integer objektumokat, hanem egy ennél nagyobb intervallumra. Ebben az intervallumban a Java runtime két azonos értékű int értéket ugyanazon Integer objektummá konvertál autoboxing esetén. Azaz

Integer a = 13;
Integer b = 13;
if( a == b ){
   System.out.println("kakukk");
}

az bizony kakukk. 13 helyett állhat itt bármilyen szám, -128 és 127 között (beleértve a határokat is), de ha -128-nál kisebb, vagy 127-nél nagyobb számot írunk, akkor bizony a != b.

A fentieket egyébiránt egészen pontosan úgy teszi a JVM, hogy amikor autoboxingot akar csinálni, akkor az Integer osztály statikus Integer.valueOf(int) metódusát hívja meg, ami kimásolva az rt.jar-ból (persze a forrás, de az is benne lehet a jar-ban):

public static Integer valueOf(int i) {
        assert IntegerCache.high >= 127;
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

Az IntegerCache az Integer osztály egy belső privát statikus osztálya, mi osztálybetöltési időben beállítja a low és high int értékeket, valamint allokálja a cache tömböt, és feltölti a -128 és 127 közötti egész értékekkel. Ennél egy kicsit bonyolultabba dolog, mert lehet definiálni a java.lang.Integer.IntegerCache.high system property-t, és ha ez definiálva van, akkor a high értéke nem 127, hanem a megadott lesz. A low értékének megváltoztatására azonban, még osztálybetöltési időben sincs a Java által támogatott lehetőség.

Ezen kívül fontos megjegyezni, hogy a low, high és a cache változók az IntegerCache osztály statikus és final változói (ez még fontos lesz).

Mit tegyünk

Essünk neki ennek az osztálynak reflection-nel! Valójában persze nem, csak a tanulság kevéért, és az itt bemutatott példát ha partner cégnél éles kódban meglátom, vagy akár csak hasonlót, akkor soha többet szóba nem állok, szerződést nem kötök.

Készítsünk egy olyan kis metódust (legyen ez egy egyetlen statikus metódus egy utility osztályban), amelyik átírja a low, high és cache mezőket a kedvünk szerint. Valahogy így:

	public static void setIntegerCacheLimits(int low, int high)
			throws NoSuchFieldException, SecurityException,
			IllegalArgumentException, IllegalAccessException {
		Class<?>[] classes = Integer.class.getDeclaredClasses();
		Class<?> cacheClass = null;
		for (Class<?> klass : classes) {
			if (klass.getSimpleName().equals("IntegerCache")) {
				cacheClass = klass;
			}
		}
		Field lowField = cacheClass.getDeclaredField("low");
		lowField.setAccessible(true);
		Field highField = cacheClass.getDeclaredField("high");
		highField.setAccessible(true);
		Field cacheField = cacheClass.getDeclaredField("cache");
		cacheField.setAccessible(true);
		Integer[] cache;
		cache = new Integer[(high - low) + 1];
		int j = low;
		for (int k = 0; k < cache.length; k++)
			cache[k] = Integer.valueOf(j++);
		cacheField.set(null, cache);
		highField.set(null, high);
		lowField.set(null, low);
	}

Működik? Nem! Bummer. Mit mond?

java.lang.IllegalAccessException: Can not set static final [Ljava.lang.Integer; field java.lang.Integer$IntegerCache.cache to [Ljava.lang.Integer;
	at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:73)
	at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:77)
	at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)
	at java.lang.reflect.Field.set(Field.java:680)
... itt még folytatódik a stack trace, nem érdekes

Miért nem megy? Hát megadtuk, hogy setAccessible(true), hozzá kellene engednie!

Mit mond erről a setAccessible() dokumentációja?


Set the accessible flag for this object to the indicated boolean value. A value of true indicates that the reflected object should suppress Java language access checking when it is used. A value of false indicates that the reflected object should enforce Java language access checks.

Hát bizony ennek engednie kellene, és pár perc guglizás után ki is derül, hogy nem a private-tal van baja, hanem a final-lal. Az ugyanis nem hozzáférési jogosultság kérés, hanem egyszerűen a final az végleges, azt nem lehet átállítani. Persze, ami ott van a memóriában, ahhoz hozzá lehet férni, és aki elszánt, azt meg nem lehet állítani. Azért, mert a reflection Field osztályának set metódusa nem akarja átírni a final mezőket azért még ne adjuk fel. Verjük át a reflection osztályt, hitessük el vele, hogy ez a mező nem final.

Hogyan? Hát reflection-nel!

	public static void setIntegerCacheLimits(int low, int high)
			throws NoSuchFieldException, SecurityException,
			IllegalArgumentException, IllegalAccessException {
		Class<?>[] classes = Integer.class.getDeclaredClasses();
		Class<?> cacheClass = null;
		for (Class<?> klass : classes) {
			if (klass.getSimpleName().equals("IntegerCache")) {
				cacheClass = klass;
			}
		}
		Field lowField = cacheClass.getDeclaredField("low");
		lowField.setAccessible(true);
		Field highField = cacheClass.getDeclaredField("high");
		highField.setAccessible(true);
		Field cacheField = cacheClass.getDeclaredField("cache");
		cacheField.setAccessible(true);
		Integer[] cache;
		cache = new Integer[(high - low) + 1];
		int j = low;
		for (int k = 0; k < cache.length; k++)
			cache[k] = Integer.valueOf(j++);
		Field modifiersField = Field.class.getDeclaredField("modifiers");
		modifiersField.setAccessible(true);
		modifiersField.setInt(cacheField, cacheField.getModifiers()
				& ~Modifier.FINAL);
		cacheField.set(null, cache);
		modifiersField.setInt(highField, highField.getModifiers()
				& ~Modifier.FINAL);
		highField.set(null, high);
		modifiersField.setInt(lowField, lowField.getModifiers()
				& ~Modifier.FINAL);
		lowField.set(null, low);
	}

Mit is csinálunk? Elkérjük reflection-nel a java.lang.reflect.Field osztály modifiers mezőjét, és az átállítani kívánt három mező esetében átállítjuk a FINAL bitet. (Sajnos a java.lang.reflect.Field osztályban a modifiers mező nem final, pedig lehetne az, és ezért ezt a trükköt meg lehet lépni.)

Innen kezdve a reflection azt hiszi, hogy ez a három mező az IntegerCache osztályban nem final és hajlandó asszisztálni abban, hogy tűkön lőjük magunkat. A metódus hiba nélkül lefut, öröm és boldogság…

… egészen addig, míg meg nem látjuk a kis teszt programunknak

package tifyty;

import java.lang.reflect.Field;

import org.junit.Test;

public class TestSetIntegerCache {
	@Test
	public void testSetIntegerCache() throws NoSuchFieldException,
			SecurityException, IllegalArgumentException, IllegalAccessException {
		SetIntegerCache.setIntegerCacheLimits(-3000, 3000);
		Class<?>[] classes = Integer.class.getDeclaredClasses();
		Class<?> cacheClass = null;
		for (Class<?> klass : classes) {
			if (klass.getSimpleName().equals("IntegerCache")) {
				cacheClass = klass;
			}
		}
		Field lowField = cacheClass.getDeclaredField("low");
		lowField.setAccessible(true);
		Field highField = cacheClass.getDeclaredField("high");
		highField.setAccessible(true);
		System.out.println("low =" + lowField.getInt(null));
		System.out.println("high=" + highField.getInt(null));
		for (int i = -3001; i < 3002; i += 1000) {
			Integer a = i;
			Integer b = i;
			System.out.println((a == b ? "==" : "!=") + i + "\n");
		}
	}
}

a kimenetét:

low =-3000
high=3000
!=-3001

!=-2001

!=-1001

==-1

==999

==1999

==2999

Na erre kellene gombot varrni!

Mi történik?

Avagy: csalatkoznék szemeim által?

Kinek higgyek? Az output-nak, vagy a debuggernek?

Ott van a

if (i >= IntegerCache.low && i <= IntegerCache.high)

sor, debuggolom, a low át van írva, a high át van írva, i a kettő között van, és mégis, mégis (sírógörcs, csodálkozás, és ámulás) az egész kifejezés értéke false, új Integer objektum jön létre.

Ilyenkor jön az, hogy elő a bájt kóddal. Csak oroszlánszívűeknek.

$ mkdir tmp
$ cd tmp
$ jar xvf /Library/Java/JavaVirtualMachines/1.7.0.jdk/Contents/Home/jre/lib/rt.jar
$ javap  -c java/lang/Integer.class >Integer.txt
$ less Integer.txt

Ebben azt láthatjuk, hogy

  public static java.lang.Integer valueOf(int);
    Code:
       0: getstatic     #34                 // Field $assertionsDisabled:Z
       3: ifne          22
       6: getstatic     #35                 // Field java/lang/Integer$IntegerCache.high:I
       9: bipush        127
      11: if_icmpge     22
      14: new           #36                 // class java/lang/AssertionError
      17: dup
      18: invokespecial #37                 // Method java/lang/AssertionError."<init>":()V
      21: athrow
      22: iload_0
      23: bipush        -128
      25: if_icmplt     45
      28: iload_0
      29: getstatic     #35                 // Field java/lang/Integer$IntegerCache.high:I
      32: if_icmpgt     45
      35: getstatic     #38                 // Field java/lang/Integer$IntegerCache.cache:[Ljava/lang/Integer;
      38: iload_0      
      39: sipush        128
      42: iadd
      43: aaload
      44: areturn      
      45: new           #39                 // class java/lang/Integer
      48: dup
      49: iload_0      
      50: invokespecial #40                 // Method "<init>":(I)V
      53: areturn      

Mit látunk a 23. sorban? Azt, hogy bipush -128. Ezt a final értéket bizony a javac befordította a kódba. Hiába létezik az IntegerCache osztályban a statikus low változó (mert létezik, aki nem hiszi, fejtse vissza azt is, és látni fogja, hogy statikus initializer a

      50: bipush        -128
      52: istore_2

sorokkal tárolja ezt az értéket). Hiába, mert a valueOf(int) NEM olvassa azt a változót. Minek is olvasná? Az konstans 127, nem is lehet más, hiszen final. És ha bármilyen final változót átállítunk reflection-nel, akkor fel lehetünk rá készülve, hogy bárhol a kódban, ha valamelyik programrészlet, JIT által generált kód darab, szál, vagy akár egy tündér, vagy törp tartogat magánál egy másolatot, eszébe nem fog jutni megnézni soha, hogy esetleg megváltozott-e a megváltoztathatatlan.

Ezért kellene, hogy a Field osztályban a modifiers mező is final legyen. És nem csak azért nem írtam bele az előző szavazásba, hogy oldjuk meg reflection-nel, mert féltem, hogy valaki arra szavaz, hanem, mert nem lehet. Meg nem is kell.

Integer cache konfigurálható. De miért billeg?

A korábbi Ez már tényleg … posztban megnéztük, hogy az Integer osztály betöltődéskor létrehoz 256 darab konstans Integer objektumot, és az autoboxing során ezt használja a -128 és 127 közötti int értékek konvertálására. Azt is megnéztük, hogy ez bizonyos esetekben lényeges sebességnövekedést hozhat. Mi a helyzet akkor, ha azt gondoljuk, hogy ez a sebességnövekedés még nagyobb is lehetne, ha ez a cache még nagyobb lenne? Mondjuk nem csak 127-ig tartalmazná a számokat, hanem akár 1000-ig.

A rövid válasz, hogy felejts el. You are doing it wrong. Olvasgasd a napi mi a f*sz cikkeit. Annyira hülyeség, ez a gondolat, hogy a kommentek egy részéről még azt sem lehet eldönteni, hogy komolyan gondolják-e.

Ennek azonban némileg ellent mond, hogy a Java fejlesztői nyitva hagytak egy lehetőséget, hogy ne csak 127-ig legyenek konstansok az Integer-ek. Ha a java.lang.Integer.IntegerCache.high értékét átállítjuk, hogy ne 127 legyen a felső limit a cache-elt Integer-ekre.

De miért csak a felső értéket lehet átállítani? Miért nem lehet az Integer osztályban egy static cache, ami -128 alatt is tárolja (cache-eli, pool-olja) az objektumokat?

A szavazásba bele sem mertem írni, hogy reflection-nel megoldható, hogy az alsó határ se -128 legyen. Még valaki képes lett volna arra szavazni.