Java Magic. Part 4: sun.misc.Unsafe
Java是一門安全的程式語言,防止程式設計師犯很多愚蠢的錯誤,它們大部分是基於記憶體管理的。但是,有一種方式可以有意的執行一些不安全、容易犯錯的操作,那就是使用Unsafe
類。
本文是sun.misc.Unsafe
公共API的簡要概述,及其一些有趣的用法。
Unsafe 例項
在使用Unsafe之前,我們需要建立Unsafe物件的例項。這並不像Unsafe unsafe = new Unsafe()
這麼簡單,因為Unsafe的
構造器是私有的。它也有一個靜態的getUnsafe()
方法,但如果你直接呼叫Unsafe.getUnsafe()
,你可能會得到SecurityException異常。只能從受信任的程式碼中使用這個方法。
public static Unsafe getUnsafe() { Class cc = sun.reflect.Reflection.getCallerClass(2); if (cc.getClassLoader() != null) throw new SecurityException("Unsafe"); return theUnsafe; }
這就是Java如何驗證程式碼是否可信。它只檢查我們的程式碼是否由主要的類載入器載入。
我們可以令我們的程式碼“受信任”。執行程式時,使用bootclasspath 選項,指定系統類路徑加上你使用的一個Unsafe路徑。
java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient
但這太難了。
Unsafe
類包含一個私有的、名為theUnsafe的例項
,我們可以通過Java反射竊取該變數。
Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null);
注意:忽略你的IDE。比如:eclipse顯示”Access restriction…”錯誤,但如果你執行程式碼,它將正常執行。如果這個錯誤提示令人煩惱,可以通過以下設定來避免:
Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecated and restricted API -> Forbidden reference -> Warning
Unsafe API
sun.misc.Unsafe類包含105個方法。實際上,對各種實體操作有幾組重要方法,其中的一些如下:
Info.僅返回一些低階的記憶體資訊
addressSize
pageSize
Objects.提供用於操作物件及其欄位的方法
allocateInstance
objectFieldOffset
Classes.提供用於操作類及其靜態欄位的方法
staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized
Arrays.運算元組
arrayBaseOffset
arrayIndexScale
Synchronization.低階的同步原語
monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt
Memory.直接記憶體訪問方法
allocateMemory
copyMemory
freeMemory
getAddress
getInt
putInt
有趣的用例
避免初始化
當你想要跳過物件初始化階段,或繞過構造器的安全檢查,或例項化一個沒有任何公共構造器的類,allocateInstance
方法是非常有用的。考慮以下類:
class A { private long a; // not initialized value public A() { this.a = 1; // initialization } public long a() { return this.a; } }
使用構造器、反射和unsafe初始化它,將得到不同的結果。
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
想想所有單例發生了什麼。
記憶體崩潰(Memory corruption)
這對於每個C程式設計師來說是常見的。順便說一下,它是繞過安全的常用技術。
考慮下那些用於檢查“訪問規則”的簡單類:
class Guard { private int ACCESS_ALLOWED = 1; public boolean giveAccess() { return 42 == ACCESS_ALLOWED; } }
客戶端程式碼是非常安全的,並且通過呼叫giveAccess()
來檢查訪問規則。可惜,對於客戶,它總是返回false。只有特權使用者可以以某種方式改變ACCESS_ALLOWED
常量的值並且得到訪問(giveAccess()方法返回true,譯者注)。
實際上,這並不是真的。演示程式碼如下:
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
現在所有的客戶都擁有無限制的訪問許可權。
實際上,反射可以實現相同的功能。但值得關注的是,我們可以修改任何物件,甚至沒有這些物件的引用。
例如,有一個guard物件,所在記憶體中的位置緊接著在當前guard物件之後。我們可以用以下程式碼來修改它的ACCESS_ALLOWED
欄位:
unsafe.putInt(guard, 16 + unsafe.objectFieldOffset(f), 42); // memory corruption
注意:我們不必持有這個物件的引用。16是Guard
物件在32位架構上的大小。我們可以手工計算它,或者通過使用sizeOf
方法(它的定義,如下節)。
sizeOf
使用objectFieldOffset
方法可以實現C-風格(C-style)的sizeof
方法。這個實現返回物件的自身記憶體大小(譯者注:shallow size)。
public static long sizeOf(Object o) { Unsafe u = getUnsafe(); HashSet<Field> fields = new HashSet<Field>(); 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 }
演算法如下:通過所有非靜態欄位(包含父類的),獲取每個欄位的偏移量(offset),找到偏移最大值並填充位元組數(padding)。我可能錯過一些東西,但思路是明確的。
如果我們僅讀取物件的類結構大小值,sizeOf的實現可以更簡單,這位於JVM 1.7 32 bit
中的偏移量12。
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包,但這需要在JVM中指定agent
選項。
淺拷貝(Shallow copy)
為了實現計算物件自身記憶體大小,我們可以簡單地新增拷貝物件方法。標準的解決方案是使用Cloneable
修改你的程式碼,或者在你的物件中實現自定義的拷貝方法,但它不會是多用途的方法。
淺拷貝:
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]; }
這個拷貝方法可以用來拷貝任何型別的物件,動態計算它的大小。注意,在拷貝後,你需要將物件轉換成特定的型別。
隱藏密碼(Hide Password)
在Unsafe
中,一個更有趣的直接記憶體訪問的用法是,從記憶體中刪除不必要的物件。
檢索使用者密碼的大多數API的簽名為byte[]
或char[],
為什麼是陣列呢?
這完全是出於安全的考慮,因為我們可以刪除不需要的陣列元素。如果將使用者密碼檢索成字串,這可以像一個物件一樣在記憶體中儲存,而刪除該物件只需執行解除引用的操作。但是,這個物件仍然在記憶體中,由GC決定的時間來執行清除。
建立具有相同大小、假的String物件,來取代在記憶體中原來的String物件的技巧:
String password = new String("[email protected]$e"); String fake = new String(password.replaceAll(".", "?")); System.out.println(password); // [email protected]$e System.out.println(fake); // ???????????? getUnsafe().copyMemory( fake, 0L, null, toAddress(password), sizeOf(password)); System.out.println(password); // ???????????? System.out.println(fake); // ????????????
感覺很安全。
修改:這並不安全。為了真正的安全,我們需要通過反射刪除後臺char陣列:
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] = '?'; }
感謝Peter Verhas指定出這一點。
多繼承(Multiple Inheritance)
Java中沒有多繼承。
這是對的,除非我們可以將任意型別轉換成我們想要的其他型別。
long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L)); long strClassAddress = normalize(getUnsafe().getInt("", 4L)); getUnsafe().putAddress(intClassAddress + 36, strClassAddress);
這個程式碼片段將String型別新增到Integer超類中,因此我們可以強制轉換,且沒有執行時異常。
(String) (Object) (new Integer(666))
有一個問題,我們必須預先強制轉換物件,以欺騙編譯器。
動態類(Dynamic classes)
我們可以在執行時建立一個類,比如從已編譯的.class檔案中。將類內容讀取為位元組陣列,並正確地傳遞給defineClass
方法。
byte[] classContents = getClassContent(); Class c = getUnsafe().defineClass( null, classContents, 0, classContents.length); c.getMethod("a").invoke(c.newInstance(), null); // 1
從定義檔案(class檔案)中讀取(程式碼)如下:
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; }
當你必須動態建立類,而現有程式碼中有一些代理, 這是很有用的。
丟擲異常(Throw an Exception)
不喜歡受檢異常?沒問題。
getUnsafe().throwException(new IOException());
該方法丟擲受檢異常,但你的程式碼不必捕捉或重新丟擲它,正如執行時異常一樣。
快速序列化(Fast Serialization)
這更有實用性。
大家都知道,標準Java的Serializable的序列化能力是非常慢的。它同時要求類必須有一個公共的、無引數的構造器。
Externalizable
比較好,但它需要定義類序列化的模式。
流行的高效能庫,比如kryo具有依賴性,這對於低記憶體要求來說是不可接受的。
unsafe類可以很容易實現完整的序列化週期。
序列化:
- 使用反射構建模式物件,類只可做一次。
- 使用
Unsafe
方法,如getLong
、getInt
、getObject
等來檢索實際欄位值。 - 新增類標識,以便有能力恢復該物件
- 將它們寫入檔案或任意輸出
你也可以新增壓縮(步驟)以節省空間。
反序列化:
- 建立已序列化物件例項,使用
allocateInstance
協助(即可),因為不需要任何構造器。 - 構建模式,與序列化的步驟1相同。
- 從檔案或任意輸入中讀取所有欄位。
- 使用
Unsafe
方法,如putLong
、putInt
、putObject
等來填充該物件。
實際上,在正確的實現過程中還有更多的細節,但思路是明確的。
這個序列化將非常快。
大陣列(Big Arrays)
正如你所知,Java陣列大小的最大值為Integer.MAX_VALUE
。使用直接記憶體分配,我們建立的陣列大小受限於堆大小。
SuperArray的實現
:
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; } }
簡單用法:
long SUPER_SIZE = (long)Integer.MAX_VALUE * 2; SuperArray array = new SuperArray(SUPER_SIZE); System.out.println("Array size:" + array.size()); // 4294967294 for (int i = 0; i < 100; i++) { array.set((long)Integer.MAX_VALUE + i, (byte)3); sum += array.get((long)Integer.MAX_VALUE + i); } System.out.println("Sum of 100 elements:" + sum); // 300
實際上,這是堆外記憶體(off-heap memory
)技術,在java.nio
包中部分可用。
這種方式的記憶體分配不在堆上,且不受GC管理,所以必須小心Unsafe.freeMemory()的使用。它也不執行任何邊界檢查,所以任何非法訪問可能會導致JVM崩潰。
這可用於數學計算,程式碼可操作大陣列的資料。此外,這可引起實時程式設計師的興趣,可打破GC在大陣列上延遲的限制。
併發(Concurrency)
幾句關於Unsafe
的併發性。compareAndSwap
方法是原子的,並且可用來實現高效能的、無鎖的資料結構。
比如,考慮問題:在使用大量執行緒的共享物件上增長值。
首先,我們定義簡單的Counter
介面:
interface Counter { void increment(); long getCounter(); }
然後,我們定義使用Counter的工作執行緒CounterClient
:
class CounterClient implements Runnable { private Counter c; private int num; public CounterClient(Counter c, int num) { this.c = c; this.num = num; } @Override public void run() { for (int i = 0; i < num; i++) { c.increment(); } } }
測試程式碼:
int NUM_OF_THREADS = 1000; int NUM_OF_INCREMENTS = 100000; ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS); Counter counter = ... // creating instance of specific counter long before = System.currentTimeMillis(); for (int i = 0; i < NUM_OF_THREADS; i++) { service.submit(new CounterClient(counter, NUM_OF_INCREMENTS)); } service.shutdown(); service.awaitTermination(1, TimeUnit.MINUTES); long after = System.currentTimeMillis(); System.out.println("Counter result: " + c.getCounter()); System.out.println("Time passed in ms:" + (after - before));
第一個無鎖版本的計數器:
class StupidCounter implements Counter { private long counter = 0; @Override public void increment() { counter++; } @Override public long getCounter() { return counter; } }
輸出:
Counter result: 99542945 Time passed in ms: 679
執行快,但沒有執行緒管理,結果是不準確的。第二次嘗試,新增上最簡單的java式同步:
class SyncCounter implements Counter { private long counter = 0; @Override public synchronized void increment() { counter++; } @Override public long getCounter() { return counter; } }
輸出:
Counter result: 100000000 Time passed in ms: 10136
激進的同步有效,但耗時長。試試ReentrantReadWriteLock
:
class LockCounter implements Counter { private long counter = 0; private WriteLock lock = new ReentrantReadWriteLock().writeLock(); @Override public void increment() { lock.lock(); counter++; lock.unlock(); } @Override public long getCounter() { return counter; } }
輸出:
Counter result: 100000000 Time passed in ms: 8065
仍然正確,耗時較短。atomics的執行效果如何?
class AtomicCounter implements Counter { AtomicLong counter = new AtomicLong(0); @Override public void increment() { counter.incrementAndGet(); } @Override public long getCounter() { return counter.get(); } }
輸出:
Counter result: 100000000 Time passed in ms: 6552
AtomicCounter的執行結果更好。最後,試試
Unsafe
原始的compareAndSwapLong
,看看它是否真的只有特權才能使用它?
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; } }
輸出:
Counter result: 100000000 Time passed in ms: 6454
看起來似乎等價於atomics。atomics使用Unsafe
?(是的)
實際上,這個例子很簡單,但它展示了Unsafe
的一些能力。
如我所說,CAS原語可以用來實現無鎖的資料結構。背後的原理很簡單:
- 有一些狀態
- 建立它的副本
- 修改它
- 執行CAS
- 如果失敗,重複嘗試
實際上,現實中比你現象的更難。存在著許多問題,如ABA問題、指令重排序等。
修改:給counter變數新增volatile
關鍵字,以避免無限迴圈的風險。
結論(Conclusion)
即使Unsafe
對應用程式很有用,但(建議)不要使用它。