java magic翻譯系列文章,java小伙伴不知道的奇妙世界
Java Magic. Part 4: sun.misc.Unsafe
@(Base)[JDK, Unsafe, magic, 黑魔法]
轉載請寫明:原文地址
系列文章:
-Java Magic. Part 1: java.net.URL
-Java Magic. Part 2: 0xCAFEBABE
-Java Magic. Part 3: Finally
-Java Magic. Part 4: sun.misc.Unsafe
英文原文
Java是一個safe programming language,它採取了很多措施來避免programmer做傻事。例如:記憶體管理。但是在Java中也提供了一種方式,讓你可以讓你做這些傻事,使用Unsafe
類。
Unsafe instantiation
在我們使用使用Unsafe
之前,我們必須先獲取一個Unsafe
的實例。當然我們不能直接Unsafe unsafe = new Unsafe()
這樣獲取,因為Unsafe
的構造函數是私有的。但是呢有一個共有的getUnsafe()
方法,但是如果你直接調用這個靜態方法的話,你可能只能收到一個SecurityException
。因為這個Unsafe類只被用於授信的類。下麵我們看下這段代碼是怎麼寫的。
public static Unsafe getUnsafe() {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
return theUnsafe;
}
這就是java代碼如何驗證代碼是否授信。他會檢查你的代碼是由哪個classLoader載入的。
JDK的包都是由Primary ClassLoader載入的
當然我們可以讓我們的代碼也變成授信的。使用bootclasspath
當啟動程式的時候,如下操作:
java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:.com.mishadoff.magic.UnsafeClient
但是這尼瑪也太討厭了吧,還有別的辦法嗎?
Unsafe
類有一個私有域叫做theUnsafe。我們可以通過反射來獲取這個引用。
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
這個時候一般的IDE都會提示你錯誤,"Access restriction"。我們可以配置忽略掉:
Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecated and restricted API -> Forbidden reference -> Warning
Unsafe API
sun.misc.Unsafe
包有105個方法。但是只有下麵幾個方法可能對你來說比較重要。
獲取信息類。獲取底層的記憶體信息:
addressSize
pageSize
操作對象。提供了一些列操作對象和欄位的方法:
allocateInstance
objectFieldOffset
操作class文件。提供了一些操作class文件的方法:
staticFieldOffset
defineClass
defineAnonymousClass
ensureClassInitialized
操作數組。
arrayBaseOffset
arrayIndexScale
同步機制。提供了底層的原子的同步機制
monitorEnter
tryMonitorEnter
monitorExit
compareAndSwapInt
putOrderedInt
直接記憶體操作
allocateMemory
copyMemory
freeMemory
getAddress
getInt
putInt
一些有趣的例子
Avoid initialization
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;
}
}
Client的代碼會被安全驗證。有趣的是,這個giveAccess()
函數永遠返回false,除非你有能力改變私有域ACCESS_ALLOWED。
顯然我們可以改變:
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
如上操作,所有的client都可以無限制的使用了。
當然上面的操作反射也可以實現。但是非常有趣的是,我們這樣操作甚至可以無需獲取對象的引用。
例如,如果我們還有一個Guard
對象在這段記憶體後面。我們可以直接操作他:
unsafe.putInt(guard, 16 + unsafe.objectFieldOffset(f), 42); // memory corruption
上述代碼我們就直接操作了這個對象,註意哈,16是Guard
對象在32位架構下的大小。其實我們可以直接使用sizeOf方法來獲取Guard
對象的大小。好吧,我們接下來就介紹sizeOf
方法
sizeOf
我們使用objectFieldOffset
可以簡單實現類似於c的sizeOf
函數。看如下代碼:
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
}
譯者註:
getDeclaredFields
並不能獲取父類的欄位,所以這個娃還操作了父類。
演算法思想如下:遍歷所有非靜態的欄位(包括所有父類的中),我們計算每一個欄位的大小。可能我有的地方寫錯了,但是思路也就大致如此。
當然還有一個更簡單的方法獲取size,我們可以直接讀取對象上的類結構,在JVM 1.7 32位架構上的偏移量是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
介面。
Shallow copy:
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[]
來存儲,為什麼使用數組?
這是出於安全原因,因為我們我們可以把數組元素清空(在我們不需要他們的時候)。如果我們使用String
來存儲,那麼當我們不用的時候設置這個引用為null,這時候只是簡單的清空引用,等待GC。
下麵就是個trick用來清理string對象:
String password = new String("l00k@myHor$e");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // l00k@myHor$e
System.out.println(fake); // ????????????
getUnsafe().copyMemory(
fake, 0L, null, toAddress(password), sizeOf(password));
System.out.println(password); // ????????????
System.out.println(fake); // ????????????
有沒有覺得安全很多~
UPDATE: 這其實這個其實並不是真正的安全了。我們必須使用反射來清除原來String中的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] = '?';
}
Multiple Inheritance
Java並不支持多繼承。當然我們也可以不停地做強制類型轉換。
long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
long strClassAddress = normalize(getUnsafe().getInt("", 4L));
getUnsafe().putAddress(intClassAddress + 36, strClassAddress);
上面這個小例子就是從String
強制轉換成Int
(如果直接強轉是有異常的)
Dynamic classes
我們可以在運行時期創建一個classes對象。如下代碼:
byte[] classContents = getClassContent();
Class c = getUnsafe().defineClass(
null, classContents, 0, classContents.length);
c.getMethod("a").invoke(c.newInstance(), null); // 1
下麵是reading file:
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
不喜歡checkedException? 沒問題!
getUnsafe().throwException(new IOException());
這個方法會拋出一個受檢的異常,但是你的代碼不會被強制要求必須catch。
Fast Serialization
這個例子非常有用喲。
所有人都知道,JAVA自帶的序列化方法非常的慢,而且還強制你的類有一個無參的構造函數。
Externalizable
會好一點,但是可能需要你自己定義class的schema。
有一個流行的高性能序列化的庫kryo
。
但是以上說的,我們通通可以使用Unsafe來處理
Serialization:
- 通過反射創建一個object的schema,這個操作每個類只需要一次。
- 使用
Unsafe
的getLong
,getInt
,getObject
來獲取實際的值 - 添加一個類的identifier
- 把這些通通寫到文件或者別的什麼裡面去
當然最後你還可以壓縮一下來減少存儲
Deserialization:
- 創建一個待反序列化的類,通過
allocateInstance
,因為這個可以不適用任何構造函數 - 創建一個schema,和序列化裡面的操作一樣啦。
- 從文件中獲取所有輸出
- 使用
Unsafe
的putLong
,putInt
,putObject
來設置實際的值
思路大致如此,但是實際的操作中還有很多很多的細節。
不過,這麼操作起來,確實很快。可以參考kryo
對Unsafe的使用,這裡
Big Arrays
大家都知道java數組的最大值就是Integer.MAX_VALUE
。我們可以使用直接記憶體分配的技術來創建無限制大小的數組。
下麵是示例代碼:
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
實際上,這使用了java堆外記憶體的技術,這個在java.nio包中有用到。
直接記憶體操作的技術可以讓我們在堆外分配記憶體,並且逃離GC的管理,所以你必須小心使用,並且使用Unsafe.freeMemory
來釋放記憶體。這個函數也不會做任何的邊界檢查,所以很容易導致JVM崩潰。
這個技巧在數學計算上非常有用。可以存取大量的數組,這個對一些realtime的programmer非常有用,如果你無法忍受大對象的GC的話,你可以自行手動操作。
Concurrency
還有一點點內容就是Unsafe.compareAndSwap
指令,所有的原子變數都是使用它來構建高性能的數據結構。
例如我們有一個簡單的Counter介面:
interface Counter {
void increment();
long getCounter();
}
下麵我們定義個一個Client來操作:
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));
第一個實現是沒有任何同步手段的Counter:
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內置的synchronization:
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
結果總是正確,但是執行時間有點讓人蛋碎。下麵我們使用讀寫鎖:
lass 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
結果正確,效率高了一點。如果使用原子變數呢?
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的方法來試驗一下:
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
哦?結果和原子變數類似,是不是原子變數就是使用Unsafe來完成操作的呢?(答案是肯定的)
顯然這些sample都很簡單,但是我們也可以從中看出Unsafe的威力。
像我說過,CAS
操作可以用來實現lock-free的數據結構,例如:
- Have some state
- Create a copy of it
- Modify it
- Perform CAS
- Repeat if it fails
實際上,這些東西實現起來非常困難,遠超你的想象,而且其中有非常多的問題,例如ABA Problem, instructions reordering, 等等。
如果你真的非常感興趣,你可以看看這篇文章:Lock-Free HashMap
Bonus
Unsafe
的park
方法,有一段非常長的英文註釋:
Block current thread, returning when a balancing unpark occurs, or a balancing unpark has already occurred, or the thread is interrupted, or, if not absolute and time is not zero, the given time nanoseconds have elapsed, or if absolute, the given deadline in milliseconds since Epoch has passed, or spuriously (i.e., returning for no "reason"). Note: This operation is in the Unsafe class only because unpark is, so it would be strange to place it elsewhere.
譯者註:park方法是用來掛起線程的,在java.concurrent.locks包下麵的AQS同步框架下應用廣泛
Conclusion
儘管Unsafe
有很牛逼的用法,但是還是不推薦使用