前言
unsafe類在jdk 源碼的多個類中用到,這個類的提供了一些繞開jvm的更底層功能,基于它的實現可以提高效率。但是,它是一把雙刃劍:正如它的名字所預示的那樣,它是unsafe的,它所分配的內存需要手動free(不被gc回收)。unsafe類,提供了jni某些功能的簡單替代:確保高效性的同時,使事情變得更簡單。
這個類是屬于sun.* api中的類,并且它不是j2se中真正的一部份,因此你可能找不到任何的官方文檔,更可悲的是,它也沒有比較好的代碼文檔。
這篇文章主要是以下文章的整理、翻譯。
http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
1. unsafe api的大部分方法都是native實現,它由105個方法組成,主要包括以下幾類:
(1)info相關。主要返回某些低級別的內存信息:addresssize(), pagesize()
(2)objects相關。主要提供object和它的域操縱方法:allocateinstance(),objectfieldoffset()
(3)class相關。主要提供class和它的靜態域操縱方法:staticfieldoffset(),defineclass(),defineanonymousclass(),ensureclassinitialized()
(4)arrays相關。數組操縱方法:arraybaseoffset(),arrayindexscale()
(5)synchronization相關。主要提供低級別同步原語(如基于cpu的cas(compare-and-swap)原語):monitorenter(),trymonitorenter(),monitorexit(),compareandswapint(),putorderedint()
(6)memory相關。直接內存訪問方法(繞過jvm堆直接操縱本地內存):allocatememory(),copymemory(),freememory(),getaddress(),getint(),putint()
2. unsafe類實例的獲取
unsafe類設計只提供給jvm信任的啟動類加載器所使用,是一個典型的單例模式類。它的實例獲取方法如下:
1
2
3
4
5
6
|
public static unsafe getunsafe() { class cc = sun.reflect.reflection.getcallerclass( 2 ); if (cc.getclassloader() != null ) throw new securityexception( "unsafe" ); return theunsafe; } |
非啟動類加載器直接調用unsafe.getunsafe()方法會拋出securityexception(具體原因涉及jvm類的雙親加載機制)。
解決辦法有兩個,其一是通過jvm參數-xbootclasspath指定要使用的類為啟動類,另外一個辦法就是java反射了。
1
2
3
|
field f = unsafe. class .getdeclaredfield( "theunsafe" ); f.setaccessible( true ); unsafe unsafe = (unsafe) f.get( null ); |
通過將private單例實例暴力設置accessible為true,然后通過field的get方法,直接獲取一個object強制轉換為unsafe。在ide中,這些方法會被標志為error,可以通過以下設置解決:
1
2
|
preferences -> java -> compiler -> errors/warnings -> deprecated and restricted api -> forbidden reference -> warning |
3. unsafe類“有趣”的應用場景
(1)繞過類初始化方法。當你想要繞過對象構造方法、安全檢查器或者沒有public的構造方法時,allocateinstance()方法變得非常有用。
1
2
3
4
5
6
7
|
class a { private long a; // not initialized value public a() { this .a = 1 ; // initialization } public long a() { return this .a; } } |
以下是構造方法、反射方法和allocateinstance()的對照
1
2
3
4
5
6
7
8
|
a o1 = new a(); // constructor o1.a(); // prints 1 a o2 = a. class .newinstance(); // reflection o2.a(); // prints 1 a o3 = (a) unsafe.allocateinstance(a. class ); // unsafe o3.a(); // prints 0 |
allocateinstance()根本沒有進入構造方法,在單例模式時,我們似乎看到了危機。
(2)內存修改
內存修改在c語言中是比較常見的,在java中,可以用它繞過安全檢查器。
考慮以下簡單準入檢查規則:
1
2
3
4
5
6
7
|
class guard { private int access_allowed = 1 ; public boolean giveaccess() { return 42 == access_allowed; } } |
在正常情況下,giveaccess總會返回false,但事情不總是這樣
1
2
3
4
5
6
7
8
9
|
guard guard = new guard(); guard.giveaccess(); // false, no access // bypass unsafe unsafe = getunsafe(); field f = guard.getclass().getdeclaredfield( "access_allowed" ); unsafe.putint(guard, unsafe.objectfieldoffset(f), 42 ); // memory corruption guard.giveaccess(); // true, access granted |
通過計算內存偏移,并使用putint()方法,類的access_allowed被修改。在已知類結構的時候,數據的偏移總是可以計算出來(與c++中的類中數據的偏移計算是一致的)。
(3)實現類似c語言的sizeof()函數
通過結合java反射和objectfieldoffset()函數實現一個c-like sizeof()函數。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public static long sizeof(object o) { unsafe u = getunsafe(); hashset fields = new hashset(); class c = o.getclass(); while (c != object. class ) { for (field f : c.getdeclaredfields()) { if ((f.getmodifiers() & modifier. static ) == 0 ) { fields.add(f); } } c = c.getsuperclass(); } // get offset long maxsize = 0 ; for (field f : fields) { long offset = u.objectfieldoffset(f); if (offset > maxsize) { maxsize = offset; } } return ((maxsize/ 8 ) + 1 ) * 8 ; // padding } |
算法的思路非常清晰:從底層子類開始,依次取出它自己和它的所有超類的非靜態域,放置到一個hashset中(重復的只計算一次,java是單繼承),然后使用objectfieldoffset()獲得一個最大偏移,最后還考慮了對齊。
在32位的jvm中,可以通過讀取class文件偏移為12的long來獲取size。
1
2
3
4
|
public static long sizeof(object object){ return getunsafe().getaddress( normalize(getunsafe().getint(object, 4l)) + 12l); } |
其中normalize()函數是一個將有符號int轉為無符號long的方法
1
2
3
4
|
private static long normalize( int value) { if (value >= 0 ) return value; return (0l >>> 32 ) & value; } |
兩個sizeof()計算的類的尺寸是一致的。最標準的sizeof()實現是使用java.lang.instrument,但是,它需要指定命令行參數-javaagent。
(4)實現java淺復制
標準的淺復制方案是實現cloneable接口或者自己實現的復制函數,它們都不是多用途的函數。通過結合sizeof()方法,可以實現淺復制。
1
2
3
4
5
6
7
|
static object shallowcopy(object obj) { long size = sizeof(obj); long start = toaddress(obj); long address = getunsafe().allocatememory(size); getunsafe().copymemory(start, address, size); return fromaddress(address); } |
以下的toaddress()和fromaddress()分別將對象轉換到它的地址以及相反操作。
1
2
3
4
5
6
7
8
9
10
11
12
|
static long toaddress(object obj) { object[] array = new object[] {obj}; long baseoffset = getunsafe().arraybaseoffset(object[]. class ); return normalize(getunsafe().getint(array, baseoffset)); } static object fromaddress( long address) { object[] array = new object[] { null }; long baseoffset = getunsafe().arraybaseoffset(object[]. class ); getunsafe().putlong(array, baseoffset, address); return array[ 0 ]; } |
以上的淺復制函數可以應用于任意java對象,它的尺寸是動態計算的。
(5)消去內存中的密碼
密碼字段存儲在string中,但是,string的回收是受到jvm管理的。最安全的做法是,在密碼字段使用完之后,將它的值覆蓋。
1
2
3
4
5
6
|
field stringvalue = string. class .getdeclaredfield( "value" ); stringvalue.setaccessible( true ); char [] mem = ( char []) stringvalue.get(password); for ( int i= 0 ; i < mem.length; i++) { mem[i] = '?' ; } |
(6)動態加載類
標準的動態加載類的方法是class.forname()(在編寫jdbc程序時,記憶深刻),使用unsafe也可以動態加載java 的class文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
byte [] classcontents = getclasscontent(); class c = getunsafe().defineclass( null , classcontents, 0 , classcontents.length); c.getmethod( "a" ).invoke(c.newinstance(), null ); // 1 getclasscontent()方法,將一個 class 文件,讀取到一個 byte 數組。 private static byte [] getclasscontent() throws exception { file f = new file( "/home/mishadoff/tmp/a.class" ); fileinputstream input = new fileinputstream(f); byte [] content = new byte [( int )f.length()]; input.read(content); input.close(); return content; } |
動態加載、代理、切片等功能中可以應用。
(7)包裝受檢異常為運行時異常。
1
|
getunsafe().throwexception( new ioexception()); |
當你不希望捕獲受檢異常時,可以這樣做(并不推薦)。
(8)快速序列化
標準的java serializable速度很慢,它還限制類必須有public無參構造函數。externalizable好些,它需要為要序列化的類指定模式。流行的高效序列化庫,比如kryo依賴于第三方庫,會增加內存的消耗。可以通過getint(),getlong(),getobject()等方法獲取類中的域的實際值,將類名稱等信息一起持久化到文件。kryo有使用unsafe的嘗試,但是沒有具體的性能提升的數據。(http://code.google.com/p/kryo/issues/detail?id=75)
(9)在非java堆中分配內存
使用java 的new會在堆中為對象分配內存,并且對象的生命周期內,會被jvm gc管理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class superarray { private final static int byte = 1 ; private long size; private long address; public superarray( long size) { this .size = size; address = getunsafe().allocatememory(size * byte ); } public void set( long i, byte value) { getunsafe().putbyte(address + i * byte , value); } public int get( long idx) { return getunsafe().getbyte(address + idx * byte ); } public long size() { return size; } } |
unsafe分配的內存,不受integer.max_value的限制,并且分配在非堆內存,使用它時,需要非常謹慎:忘記手動回收時,會產生內存泄露;非法的地址訪問時,會導致jvm崩潰。在需要分配大的連續區域、實時編程(不能容忍jvm延遲)時,可以使用它。java.nio使用這一技術。
(10)java并發中的應用
通過使用unsafe.compareandswap()可以用來實現高效的無鎖數據結構。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class cascounter implements counter { private volatile long counter = 0 ; private unsafe unsafe; private long offset; public cascounter() throws exception { unsafe = getunsafe(); offset = unsafe.objectfieldoffset(cascounter. class .getdeclaredfield( "counter" )); } @override public void increment() { long before = counter; while (!unsafe.compareandswaplong( this , offset, before, before + 1 )) { before = counter; } } @override public long getcounter() { return counter; } } |
通過測試,以上數據結構與java的原子變量的效率基本一致,java原子變量也使用unsafe的compareandswap()方法,而這個方法最終會對應到cpu的對應原語,因此,它的效率非常高。這里有一個實現無鎖hashmap的方案(http://www.azulsystems.com/about_us/presentations/lock-free-hash ,這個方案的思路是:分析各個狀態,創建拷貝,修改拷貝,使用cas原語,自旋鎖),在普通的服務器機器(核心<32),使用concurrenthashmap(jdk8以前,默認16路分離鎖實現,jdk8中concurrenthashmap已經使用無鎖實現)明顯已經夠用。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。
原文鏈接:https://www.cnblogs.com/suxuan/p/4948608.html