翻譯自:http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
Unsafe類在jdk 源碼的多個(gè)類中用到,這個(gè)類的提供了一些繞開JVM的更底層功能,基于它的實(shí)現(xiàn)可以提高效率。但是,它是一把雙刃劍:正如它的名字所預(yù)示的那樣,它是Unsafe的,它所分配的內(nèi)存需要手動(dòng)free(不被GC回收)。Unsafe類,提供了JNI某些功能的簡單替代:確保高效性的同時(shí),使事情變得更簡單。
這篇文章主要是以下文章的整理、翻譯。
http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
1. Unsafe API的大部分方法都是native實(shí)現(xiàn),它由105個(gè)方法組成,主要包括以下幾類:
(1)Info相關(guān)。主要返回某些低級別的內(nèi)存信息:addressSize(), pageSize()
(2)Objects相關(guān)。主要提供Object和它的域操縱方法:allocateInstance(),objectFieldOffset()
(3)Class相關(guān)。主要提供Class和它的靜態(tài)域操縱方法:staticFieldOffset(),defineClass(),defineAnonymousClass(),ensureClassInitialized()
(4)Arrays相關(guān)。數(shù)組操縱方法:arrayBaseOffset(),arrayIndexScale()
(5)Synchronization相關(guān)。主要提供低級別同步原語(如基于CPU的CAS(Compare-And-Swap)原語):monitorEnter(),tryMonitorEnter(),monitorExit(),compareAndSwapInt(),putOrderedInt()
(6)Memory相關(guān)。直接內(nèi)存訪問方法(繞過JVM堆直接操縱本地內(nèi)存):allocateMemory(),copyMemory(),freeMemory(),getAddress(),getInt(),putInt()
2. Unsafe類實(shí)例的獲取
Unsafe類設(shè)計(jì)只提供給JVM信任的啟動(dòng)類加載器所使用,是一個(gè)典型的單例模式類。它的實(shí)例獲取方法如下:
publicstaticUnsafegetUnsafe(){Classcc=sun.reflect.Reflection.getCallerClass(2);if(cc.getClassLoader()!=null)thrownewSecurityException("Unsafe");returntheUnsafe;}
非啟動(dòng)類加載器直接調(diào)用Unsafe.getUnsafe()方法會(huì)拋出SecurityException(具體原因涉及JVM類的雙親加載機(jī)制)。
解決辦法有兩個(gè),其一是通過JVM參數(shù)-Xbootclasspath指定要使用的類為啟動(dòng)類,另外一個(gè)辦法就是java反射了。
Fieldf=Unsafe.class.getDeclaredField("theUnsafe");f.setAccessible(true);Unsafeunsafe=(Unsafe)f.get(null);
通過將private單例實(shí)例暴力設(shè)置accessible為true,然后通過Field的get方法,直接獲取一個(gè)Object強(qiáng)制轉(zhuǎn)換為Unsafe。在IDE中,這些方法會(huì)被標(biāo)志為Error,可以通過以下設(shè)置解決:
Preferences -> Java -> Compiler -> Errors/Warnings ->
Deprecated and restricted API -> Forbidden reference -> Warning
3. Unsafe類“有趣”的應(yīng)用場景
(1)繞過類初始化方法。當(dāng)你想要繞過對象構(gòu)造方法、安全檢查器或者沒有public的構(gòu)造方法時(shí),allocateInstance()方法變得非常有用。
classA{privatelonga;// not initialized valuepublicA(){this.a=1;// initialization}publiclonga(){returnthis.a;}}
以下是構(gòu)造方法、反射方法和allocateInstance()的對照
Ao1=newA();// constructoro1.a();// prints 1Ao2=A.class.newInstance();// reflectiono2.a();// prints 1Ao3=(A)unsafe.allocateInstance(A.class);// unsafeo3.a();// prints 0
allocateInstance()根本沒有進(jìn)入構(gòu)造方法,在單例模式時(shí),我們似乎看到了危機(jī)。
(2)內(nèi)存修改
內(nèi)存修改在c語言中是比較常見的,在Java中,可以用它繞過安全檢查器。
考慮以下簡單準(zhǔn)入檢查規(guī)則:
classGuard{privateintACCESS_ALLOWED=1;publicbooleangiveAccess(){return42==ACCESS_ALLOWED;}}
在正常情況下,giveAccess總會(huì)返回false,但事情不總是這樣
Guardguard=newGuard();guard.giveAccess();// false, no access// bypassUnsafeunsafe=getUnsafe();Fieldf=guard.getClass().getDeclaredField("ACCESS_ALLOWED");unsafe.putInt(guard,unsafe.objectFieldOffset(f),42);// memory corruptionguard.giveAccess();// true, access granted
通過計(jì)算內(nèi)存偏移,并使用putInt()方法,類的ACCESS_ALLOWED被修改。在已知類結(jié)構(gòu)的時(shí)候,數(shù)據(jù)的偏移總是可以計(jì)算出來(與c++中的類中數(shù)據(jù)的偏移計(jì)算是一致的)。
(3)實(shí)現(xiàn)類似C語言的sizeOf()函數(shù)
通過結(jié)合Java反射和objectFieldOffset()函數(shù)實(shí)現(xiàn)一個(gè)C-like sizeOf()函數(shù)。
publicstaticlongsizeOf(Objecto){Unsafeu=getUnsafe();HashSetfields=newHashSet();Classc=o.getClass();while(c!=Object.class){for(Fieldf:c.getDeclaredFields()){if((f.getModifiers()&Modifier.STATIC)==0){fields.add(f);}}c=c.getSuperclass();}// get offsetlongmaxSize=0;for(Fieldf:fields){longoffset=u.objectFieldOffset(f);if(offset>maxSize){maxSize=offset;}}return((maxSize/8)+1)*8;// padding}
算法的思路非常清晰:從底層子類開始,依次取出它自己和它的所有超類的非靜態(tài)域,放置到一個(gè)HashSet中(重復(fù)的只計(jì)算一次,Java是單繼承),然后使用objectFieldOffset()獲得一個(gè)最大偏移,最后還考慮了對齊。
在32位的JVM中,可以通過讀取class文件偏移為12的long來獲取size。
publicstaticlongsizeOf(Objectobject){returngetUnsafe().getAddress(normalize(getUnsafe().getInt(object,4L))+12L);}
其中normalize()函數(shù)是一個(gè)將有符號int轉(zhuǎn)為無符號long的方法
privatestaticlongnormalize(intvalue){if(value>=0)returnvalue;return(0L>>>32)&value;}
兩個(gè)sizeOf()計(jì)算的類的尺寸是一致的。最標(biāo)準(zhǔn)的sizeOf()實(shí)現(xiàn)是使用java.lang.instrument,但是,它需要指定命令行參數(shù)-javaagent。
(4)實(shí)現(xiàn)Java淺復(fù)制
標(biāo)準(zhǔn)的淺復(fù)制方案是實(shí)現(xiàn)Cloneable接口或者自己實(shí)現(xiàn)的復(fù)制函數(shù),它們都不是多用途的函數(shù)。通過結(jié)合sizeOf()方法,可以實(shí)現(xiàn)淺復(fù)制。
staticObjectshallowCopy(Objectobj){longsize=sizeOf(obj);longstart=toAddress(obj);longaddress=getUnsafe().allocateMemory(size);getUnsafe().copyMemory(start,address,size);returnfromAddress(address);}
以下的toAddress()和fromAddress()分別將對象轉(zhuǎn)換到它的地址以及相反操作。
staticlongtoAddress(Objectobj){Object[]array=newObject[]{obj};longbaseOffset=getUnsafe().arrayBaseOffset(Object[].class);returnnormalize(getUnsafe().getInt(array,baseOffset));}staticObjectfromAddress(longaddress){Object[]array=newObject[]{null};longbaseOffset=getUnsafe().arrayBaseOffset(Object[].class);getUnsafe().putLong(array,baseOffset,address);returnarray[0];}
以上的淺復(fù)制函數(shù)可以應(yīng)用于任意java對象,它的尺寸是動(dòng)態(tài)計(jì)算的。
(5)消去內(nèi)存中的密碼
密碼字段存儲(chǔ)在String中,但是,String的回收是受到JVM管理的。最安全的做法是,在密碼字段使用完之后,將它的值覆蓋。
FieldstringValue=String.class.getDeclaredField("value");stringValue.setAccessible(true);char[]mem=(char[])stringValue.get(password);for(inti=0;i
(6)動(dòng)態(tài)加載類
標(biāo)準(zhǔn)的動(dòng)態(tài)加載類的方法是Class.forName()(在編寫jdbc程序時(shí),記憶深刻),使用Unsafe也可以動(dòng)態(tài)加載java 的class文件。
byte[]classContents=getClassContent();Classc=getUnsafe().defineClass(null,classContents,0,classContents.length);c.getMethod("a").invoke(c.newInstance(),null);// 1getClassContent()方法,將一個(gè)class文件,讀取到一個(gè)byte數(shù)組。privatestaticbyte[]getClassContent()throwsException{Filef=newFile("/home/mishadoff/tmp/A.class");FileInputStreaminput=newFileInputStream(f);byte[]content=newbyte[(int)f.length()];input.read(content);input.close();returncontent;}
動(dòng)態(tài)加載、代理、切片等功能中可以應(yīng)用。
(7)包裝受檢異常為運(yùn)行時(shí)異常。
getUnsafe().throwException(newIOException());
當(dāng)你不希望捕獲受檢異常時(shí),可以這樣做(并不推薦)。
(8)快速序列化
標(biāo)準(zhǔn)的java Serializable速度很慢,它還限制類必須有public無參構(gòu)造函數(shù)。Externalizable好些,它需要為要序列化的類指定模式。流行的高效序列化庫,比如kryo依賴于第三方庫,會(huì)增加內(nèi)存的消耗??梢酝ㄟ^getInt(),getLong(),getObject()等方法獲取類中的域的實(shí)際值,將類名稱等信息一起持久化到文件。kryo有使用Unsafe的嘗試,但是沒有具體的性能提升的數(shù)據(jù)。(http://code.google.com/p/kryo/issues/detail?id=75)
(9)在非Java堆中分配內(nèi)存
使用java 的new會(huì)在堆中為對象分配內(nèi)存,并且對象的生命周期內(nèi),會(huì)被JVM GC管理。
classSuperArray{privatefinalstaticintBYTE=1;privatelongsize;privatelongaddress;publicSuperArray(longsize){this.size=size;address=getUnsafe().allocateMemory(size*BYTE);}publicvoidset(longi,bytevalue){getUnsafe().putByte(address+i*BYTE,value);}publicintget(longidx){returngetUnsafe().getByte(address+idx*BYTE);}publiclongsize(){returnsize;}}
Unsafe分配的內(nèi)存,不受Integer.MAX_VALUE的限制,并且分配在非堆內(nèi)存,使用它時(shí),需要非常謹(jǐn)慎:忘記手動(dòng)回收時(shí),會(huì)產(chǎn)生內(nèi)存泄露;非法的地址訪問時(shí),會(huì)導(dǎo)致JVM崩潰。在需要分配大的連續(xù)區(qū)域、實(shí)時(shí)編程(不能容忍JVM延遲)時(shí),可以使用它。java.nio使用這一技術(shù)。
(10)Java并發(fā)中的應(yīng)用
通過使用Unsafe.compareAndSwap()可以用來實(shí)現(xiàn)高效的無鎖數(shù)據(jù)結(jié)構(gòu)。
classCASCounterimplementsCounter{privatevolatilelongcounter=0;privateUnsafeunsafe;privatelongoffset;publicCASCounter()throwsException{unsafe=getUnsafe();offset=unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));}@Overridepublicvoidincrement(){longbefore=counter;while(!unsafe.compareAndSwapLong(this,offset,before,before+1)){before=counter;}}@OverridepubliclonggetCounter(){returncounter;}}
通過測試,以上數(shù)據(jù)結(jié)構(gòu)與java的原子變量的效率基本一致,Java原子變量也使用Unsafe的compareAndSwap()方法,而這個(gè)方法最終會(huì)對應(yīng)到cpu的對應(yīng)原語,因此,它的效率非常高。這里有一個(gè)實(shí)現(xiàn)無鎖HashMap的方案(http://www.azulsystems.com/about_us/presentations/lock-free-hash ,這個(gè)方案的思路是:分析各個(gè)狀態(tài),創(chuàng)建拷貝,修改拷貝,使用CAS原語,自旋鎖),在普通的服務(wù)器機(jī)器(核心<32),使用ConcurrentHashMap(JDK8以前,默認(rèn)16路分離鎖實(shí)現(xiàn),JDK8中ConcurrentHashMap已經(jīng)使用無鎖實(shí)現(xiàn))明顯已經(jīng)夠用。