說說Java的Unsafe類
前言
Unsafe是Java中一個底層類,包含了很多基礎的操作,比如陣列操作、物件操作、記憶體操作、CAS操作、執行緒(park)操作、柵欄(Fence)操作,JUC包、一些三方框架都使用Unsafe類來保證併發安全。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信任的啟動類載入器所使用,是一個典型的單例模式類。它的例項獲取方法如下:
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
非啟動類載入器直接呼叫Unsafe.getUnsafe()方法會丟擲SecurityException(具體原因涉及JVM類的雙親載入機制)。解決辦法有兩個,其一是通過JVM引數-Xbootclasspath指定要使用的類為啟動類,另外一個辦法就是java反射了。
Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null);
通過將private單例例項暴力設定accessible為true,然後通過Field的get方法,直接獲取一個Object強制轉換為Unsafe。在IDE中,這些方法會被標誌為Error,可以通過以下設定解決:
Preferences -> Java -> Compiler -> Errors/Warnings ->
Deprecated and restricted API -> Forbidden reference -> Warning
3. Unsafe類“有趣”的應用場景
(1)繞過類初始化方法。當你想要繞過物件構造方法、安全檢查器或者沒有public的構造方法時,allocateInstance()方法變得非常有用。
class A { private long a; // not initialized value public A() { this.a = 1; // initialization } public long a() { return this.a; } }
以下是構造方法、反射方法和allocateInstance()的對照
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中,可以用它繞過安全檢查器。考慮以下簡單准入檢查規則:
class Guard { private int ACCESS_ALLOWED = 1; public boolean giveAccess() { return 42 == ACCESS_ALLOWED; } }
在正常情況下,giveAccess總會返回false,但事情不總是這樣
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()函式。
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。
public static long sizeOf(Object object){ return getUnsafe().getAddress( normalize(getUnsafe().getInt(object, 4L)) + 12L); }
其中normalize()函式是一個將有符號int轉為無符號long的方法
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()方法,可以實現淺複製。
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()分別將物件轉換到它的地址以及相反操作。
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管理的。最安全的做法是,在密碼欄位使用完之後,將它的值覆蓋。
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檔案。
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)包裝受檢異常為執行時異常。
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管理。
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()可以用來實現高效的無鎖資料結構。
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已經使用無鎖實現)明顯已經夠用。
郭慕榮部落格園