原文地址 譯者:許巧輝 校對:梁海艦
Java是一門安全的編程語言,防止程序員犯很多愚蠢的錯誤,它們大部分是基於內存管理的。但是,有一種方式可以有意的執行一些不安全、容易犯錯的操作,那就是使用Unsafe
類。
本文是sun.misc.Unsafe
公共API的簡要概述,及其一些有趣的用法。
Unsafe 實例
在使用Unsafe之前,我們需要創建Unsafe對象的實例。這並不像Unsafe unsafe = new Unsafe()
這么簡單,因為Unsafe的
構造器是私有的。它也有一個靜態的getUnsafe()
方法,但如果你直接調用Unsafe.getUnsafe()
,你可能會得到SecurityException異常。只能從受信任的代碼中使用這個方法。
1 |
public static Unsafe getUnsafe() { |
2 |
Class cc = sun.reflect.Reflection.getCallerClass( 2 ); |
3 |
if (cc.getClassLoader() != null ) |
4 |
throw new SecurityException( "Unsafe" ); |
5 |
return theUnsafe; |
6 |
} |
這就是Java如何驗證代碼是否可信。它只檢查我們的代碼是否由主要的類加載器加載。
我們可以令我們的代碼“受信任”。運行程序時,使用bootclasspath 選項,指定系統類路徑加上你使用的一個Unsafe路徑。
1 |
java -Xbootclasspath:/usr/jdk1. 7.0 /jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient |
但這太難了。
Unsafe
類包含一個私有的、名為theUnsafe的實例
,我們可以通過Java反射竊取該變量。
1 |
Field f = Unsafe. class .getDeclaredField( "theUnsafe" ); |
2 |
f.setAccessible( true ); |
3 |
Unsafe unsafe = (Unsafe) f.get( null ); |
注意:忽略你的IDE。比如:eclipse顯示”Access restriction…”錯誤,但如果你運行代碼,它將正常運行。如果這個錯誤提示令人煩惱,可以通過以下設置來避免:
1 |
Preferences -> Java -> Compiler -> Errors/Warnings -> |
2 |
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
方法是非常有用的。考慮以下類:
1 |
class A { |
2 |
private long a; // not initialized value |
3 |
4 |
public A() { |
5 |
this .a = 1 ; // initialization |
6 |
} |
7 |
8 |
public long a() { return this .a; } |
9 |
} |
使用構造器、反射和unsafe初始化它,將得到不同的結果。
1 |
A o1 = new A(); // constructor |
2 |
o1.a(); // prints 1 |
3 |
4 |
A o2 = A. class .newInstance(); // reflection |
5 |
o2.a(); // prints 1 |
6 |
7 |
A o3 = (A) unsafe.allocateInstance(A. class ); // unsafe |
8 |
o3.a(); // prints 0 |
想想所有單例發生了什么。
內存崩潰(Memory corruption)
這對於每個C程序員來說是常見的。順便說一下,它是繞過安全的常用技術。
考慮下那些用於檢查“訪問規則”的簡單類:
1 |
class Guard { |
2 |
private int ACCESS_ALLOWED = 1 ; |
3 |
4 |
public boolean giveAccess() { |
5 |
return 42 == ACCESS_ALLOWED; |
6 |
} |
7 |
} |
客戶端代碼是非常安全的,並且通過調用giveAccess()
來檢查訪問規則。可惜,對於客戶,它總是返回false。只有特權用戶可以以某種方式改變ACCESS_ALLOWED
常量的值並且得到訪問(giveAccess()方法返回true,譯者注)。
實際上,這並不是真的。演示代碼如下:
1 |
Guard guard = new Guard(); |
2 |
guard.giveAccess(); // false, no access |
3 |
4 |
// bypass |
5 |
Unsafe unsafe = getUnsafe(); |
6 |
Field f = guard.getClass().getDeclaredField( "ACCESS_ALLOWED" ); |
7 |
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42 ); // memory corruption |
8 |
9 |
guard.giveAccess(); // true, access granted |
現在所有的客戶都擁有無限制的訪問權限。
實際上,反射可以實現相同的功能。但值得關注的是,我們可以修改任何對象,甚至沒有這些對象的引用。
例如,有一個guard對象,所在內存中的位置緊接着在當前guard對象之后。我們可以用以下代碼來修改它的ACCESS_ALLOWED
字段:
1 |
unsafe.putInt(guard, 16 + unsafe.objectFieldOffset(f), 42 ); // memory corruption |
注意:我們不必持有這個對象的引用。16是Guard
對象在32位架構上的大小。我們可以手工計算它,或者通過使用sizeOf
方法(它的定義,如下節)。
sizeOf
使用objectFieldOffset
方法可以實現C-風格(C-style)的sizeof
方法。這個實現返回對象的自身內存大小(譯者注:shallow size)。
01 |
public static long sizeOf(Object o) { |
02 |
Unsafe u = getUnsafe(); |
03 |
HashSet<Field> fields = new HashSet<Field>(); |
04 |
Class c = o.getClass(); |
05 |
while (c != Object. class ) { |
06 |
for (Field f : c.getDeclaredFields()) { |
07 |
if ((f.getModifiers() & Modifier.STATIC) == 0 ) { |
08 |
fields.add(f); |
09 |
} |
10 |
} |
11 |
c = c.getSuperclass(); |
12 |
} |
13 |
14 |
// get offset |
15 |
long maxSize = 0 ; |
16 |
for (Field f : fields) { |
17 |
long offset = u.objectFieldOffset(f); |
18 |
if (offset > maxSize) { |
19 |
maxSize = offset; |
20 |
} |
21 |
} |
22 |
23 |
return ((maxSize/ 8 ) + 1 ) * 8 ; // padding |
24 |
} |
算法如下:通過所有非靜態字段(包含父類的),獲取每個字段的偏移量(offset),找到偏移最大值並填充字節數(padding)。我可能錯過一些東西,但思路是明確的。
如果我們僅讀取對象的類結構大小值,sizeOf的實現可以更簡單,這位於JVM 1.7 32 bit
中的偏移量12。
1 |
public static long sizeOf(Object object){ |
2 |
return getUnsafe().getAddress( |
3 |
normalize(getUnsafe().getInt(object, 4L)) + 12L); |
4 |
} |
normalize
是一個為了正確內存地址使用,將有符號的int類型強制轉換成無符號的long類型的方法。
1 |
private static long normalize( int value) { |
2 |
if (value >= 0 ) return value; |
3 |
return (~0L >>> 32 ) & value; |
4 |
} |
真棒,這個方法返回的結果與我們之前的sizeof方法一樣。
實際上,對於良好、安全、准確的sizeof方法,最好使用 java.lang.instrument包,但這需要在JVM中指定agent
選項。
淺拷貝(Shallow copy)
為了實現計算對象自身內存大小,我們可以簡單地添加拷貝對象方法。標准的解決方案是使用Cloneable
修改你的代碼,或者在你的對象中實現自定義的拷貝方法,但它不會是多用途的方法。
淺拷貝:
1 |
static Object shallowCopy(Object obj) { |
2 |
long size = sizeOf(obj); |
3 |
long start = toAddress(obj); |
4 |
long address = getUnsafe().allocateMemory(size); |
5 |
getUnsafe().copyMemory(start, address, size); |
6 |
return fromAddress(address); |
7 |
} |
toAddress和
fromAddress
將對象轉換為其在內存中的地址,反之亦然。
01 |
static long toAddress(Object obj) { |
02 |
Object[] array = new Object[] {obj}; |
03 |
long baseOffset = getUnsafe().arrayBaseOffset(Object[]. class ); |
04 |
return normalize(getUnsafe().getInt(array, baseOffset)); |
05 |
} |
06 |
07 |
static Object fromAddress( long address) { |
08 |
Object[] array = new Object[] { null }; |
09 |
long baseOffset = getUnsafe().arrayBaseOffset(Object[]. class ); |
10 |
getUnsafe().putLong(array, baseOffset, address); |
11 |
return array[ 0 ]; |
12 |
} |
這個拷貝方法可以用來拷貝任何類型的對象,動態計算它的大小。注意,在拷貝后,你需要將對象轉換成特定的類型。
隱藏密碼(Hide Password)
在Unsafe
中,一個更有趣的直接內存訪問的用法是,從內存中刪除不必要的對象。
檢索用戶密碼的大多數API的簽名為byte[]
或char[],
為什么是數組呢?
這完全是出於安全的考慮,因為我們可以刪除不需要的數組元素。如果將用戶密碼檢索成字符串,這可以像一個對象一樣在內存中保存,而刪除該對象只需執行解除引用的操作。但是,這個對象仍然在內存中,由GC決定的時間來執行清除。
創建具有相同大小、假的String對象,來取代在內存中原來的String對象的技巧:
01 |
String password = new String( "l00k@myHor$e" ); |
02 |
String fake = new String(password.replaceAll( "." , "?" )); |
03 |
System.out.println(password); // l00k@myHor$e |
04 |
System.out.println(fake); // ???????????? |
05 |
06 |
getUnsafe().copyMemory( |
07 |
fake, 0L, null , toAddress(password), sizeOf(password)); |
08 |
09 |
System.out.println(password); // ???????????? |
10 |
System.out.println(fake); // ???????????? |
感覺很安全。
修改:這並不安全。為了真正的安全,我們需要通過反射刪除后台char數組:
1 |
Field stringValue = String. class .getDeclaredField( "value" ); |
2 |
stringValue.setAccessible( true ); |
3 |
char [] mem = ( char []) stringValue.get(password); |
4 |
for ( int i= 0 ; i < mem.length; i++) { |
5 |
mem[i] = '?' ; |
6 |
} |
感謝Peter Verhas指定出這一點。
多繼承(Multiple Inheritance)
Java中沒有多繼承。
這是對的,除非我們可以將任意類型轉換成我們想要的其他類型。
1 |
long intClassAddress = normalize(getUnsafe().getInt( new Integer( 0 ), 4L)); |
2 |
long strClassAddress = normalize(getUnsafe().getInt( "" , 4L)); |
3 |
getUnsafe().putAddress(intClassAddress + 36 , strClassAddress); |
這個代碼片段將String類型添加到Integer超類中,因此我們可以強制轉換,且沒有運行時異常。
1 |
(String) (Object) ( new Integer( 666 )) |
有一個問題,我們必須預先強制轉換對象,以欺騙編譯器。
動態類(Dynamic classes)
我們可以在運行時創建一個類,比如從已編譯的.class文件中。將類內容讀取為字節數組,並正確地傳遞給defineClass
方法。
1 |
byte [] classContents = getClassContent(); |
2 |
Class c = getUnsafe().defineClass( |
3 |
null , classContents, 0 , classContents.length); |
4 |
c.getMethod( "a" ).invoke(c.newInstance(), null ); // 1 |
從定義文件(class文件)中讀取(代碼)如下:
1 |
private static byte [] getClassContent() throws Exception { |
2 |
File f = new File( "/home/mishadoff/tmp/A.class" ); |
3 |
FileInputStream input = new FileInputStream(f); |
4 |
byte [] content = new byte [( int )f.length()]; |
5 |
input.read(content); |
6 |
input.close(); |
7 |
return content; |
8 |
} |
當你必須動態創建類,而現有代碼中有一些代理, 這是很有用的。
拋出異常(Throw an Exception)
不喜歡受檢異常?沒問題。
1 |
getUnsafe().throwException( new IOException()); |
該方法拋出受檢異常,但你的代碼不必捕捉或重新拋出它,正如運行時異常一樣。
快速序列化(Fast Serialization)
這更有實用性。
大家都知道,標准Java的Serializable的序列化能力是非常慢的。它同時要求類必須有一個公共的、無參數的構造器。
Externalizable
比較好,但它需要定義類序列化的模式。
流行的高性能庫,比如kryo具有依賴性,這對於低內存要求來說是不可接受的。
unsafe類可以很容易實現完整的序列化周期。
序列化:
- 使用反射構建模式對象,類只可做一次。
- 使用
Unsafe
方法,如getLong
、getInt
、getObject
等來檢索實際字段值。 - 添加類標識,以便有能力恢復該對象
- 將它們寫入文件或任意輸出
你也可以添加壓縮(步驟)以節省空間。
反序列化:
- 創建已序列化對象實例,使用
allocateInstance
協助(即可),因為不需要任何構造器。 - 構建模式,與序列化的步驟1相同。
- 從文件或任意輸入中讀取所有字段。
- 使用
Unsafe
方法,如putLong
、putInt
、putObject
等來填充該對象。
實際上,在正確的實現過程中還有更多的細節,但思路是明確的。
這個序列化將非常快。
順便說一下,在kryo中有使用Unsafe
的一些嘗試http://code.google.com/p/kryo/issues/detail?id=75
大數組(Big Arrays)
正如你所知,Java數組大小的最大值為Integer.MAX_VALUE
。使用直接內存分配,我們創建的數組大小受限於堆大小。
SuperArray的實現
:
01 |
class SuperArray { |
02 |
private final static int BYTE = 1 ; |
03 |
04 |
private long size; |
05 |
private long address; |
06 |
07 |
public SuperArray( long size) { |
08 |
this .size = size; |
09 |
address = getUnsafe().allocateMemory(size * BYTE); |
10 |
} |
11 |
12 |
public void set( long i, byte value) { |
13 |
getUnsafe().putByte(address + i * BYTE, value); |
14 |
} |
15 |
16 |
public int get( long idx) { |
17 |
return getUnsafe().getByte(address + idx * BYTE); |
18 |
} |
19 |
20 |
public long size() { |
21 |
return size; |
22 |
} |
23 |
} |
簡單用法:
1 |
long SUPER_SIZE = ( long )Integer.MAX_VALUE * 2 ; |
2 |
SuperArray array = new SuperArray(SUPER_SIZE); |
3 |
System.out.println( "Array size:" + array.size()); // 4294967294 |
4 |
for ( int i = 0 ; i < 100 ; i++) { |
5 |
array.set(( long )Integer.MAX_VALUE + i, ( byte ) 3 ); |
6 |
sum += array.get(( long )Integer.MAX_VALUE + i); |
7 |
} |
8 |
System.out.println( "Sum of 100 elements:" + sum); // 300 |
實際上,這是堆外內存(off-heap memory
)技術,在java.nio
包中部分可用。
這種方式的內存分配不在堆上,且不受GC管理,所以必須小心Unsafe.freeMemory()的使用。它也不執行任何邊界檢查,所以任何非法訪問可能會導致JVM崩潰。
這可用於數學計算,代碼可操作大數組的數據。此外,這可引起實時程序員的興趣,可打破GC在大數組上延遲的限制。
並發(Concurrency)
幾句關於Unsafe
的並發性。compareAndSwap
方法是原子的,並且可用來實現高性能的、無鎖的數據結構。
比如,考慮問題:在使用大量線程的共享對象上增長值。
首先,我們定義簡單的Counter
接口:
1 |
interface Counter { |
2 |
void increment(); |
3 |
long getCounter(); |
4 |
} |
然后,我們定義使用Counter的工作線程CounterClient
:
01 |
class CounterClient implements Runnable { |
02 |
private Counter c; |
03 |
private int num; |
04 |
05 |
public CounterClient(Counter c, int num) { |
06 |
this .c = c; |
07 |
this .num = num; |
08 |
} |
09 |
10 |
@Override |
11 |
public void run() { |
12 |
for ( int i = 0 ; i < num; i++) { |
13 |
c.increment(); |
14 |
} |
15 |
} |
16 |
} |
測試代碼:
01 |
int NUM_OF_THREADS = 1000 ; |
02 |
int NUM_OF_INCREMENTS = 100000 ; |
03 |
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS); |
04 |
Counter counter = ... // creating instance of specific counter |
05 |
long before = System.currentTimeMillis(); |
06 |
for ( int i = 0 ; i < NUM_OF_THREADS; i++) { |
07 |
service.submit( new CounterClient(counter, NUM_OF_INCREMENTS)); |
08 |
} |
09 |
service.shutdown(); |
10 |
service.awaitTermination( 1 , TimeUnit.MINUTES); |
11 |
long after = System.currentTimeMillis(); |
12 |
System.out.println( "Counter result: " + c.getCounter()); |
13 |
System.out.println( "Time passed in ms:" + (after - before)); |
第一個無鎖版本的計數器:
01 |
class StupidCounter implements Counter { |
02 |
private long counter = 0 ; |
03 |
04 |
@Override |
05 |
public void increment() { |
06 |
counter++; |
07 |
} |
08 |
09 |
@Override |
10 |
public long getCounter() { |
11 |
return counter; |
12 |
} |
13 |
} |
輸出:
1 |
Counter result: 99542945 |
2 |
Time passed in ms: 679 |
運行快,但沒有線程管理,結果是不准確的。第二次嘗試,添加上最簡單的java式同步:
01 |
class SyncCounter implements Counter { |
02 |
private long counter = 0 ; |
03 |
04 |
@Override |
05 |
public synchronized void increment() { |
06 |
counter++; |
07 |
} |
08 |
09 |
@Override |
10 |
public long getCounter() { |
11 |
return counter; |
12 |
} |
13 |
} |
輸出:
1 |
Counter result: 100000000 |
2 |
Time passed in ms: 10136 |
激進的同步有效,但耗時長。試試ReentrantReadWriteLock
:
01 |
class LockCounter implements Counter { |
02 |
private long counter = 0 ; |
03 |
private WriteLock lock = new ReentrantReadWriteLock().writeLock(); |
04 |
05 |
@Override |
06 |
public void increment() { |
07 |
lock.lock(); |
08 |
counter++; |
09 |
lock.unlock(); |
10 |
} |
11 |
12 |
@Override |
13 |
public long getCounter() { |
14 |
return counter; |
15 |
} |
16 |
} |
輸出:
1 |
Counter result: 100000000 |
2 |
Time passed in ms: 8065 |
仍然正確,耗時較短。atomics的運行效果如何?
01 |
class AtomicCounter implements Counter { |
02 |
AtomicLong counter = new AtomicLong( 0 ); |
03 |
04 |
@Override |
05 |
public void increment() { |
06 |
counter.incrementAndGet(); |
07 |
} |
08 |
09 |
@Override |
10 |
public long getCounter() { |
11 |
return counter.get(); |
12 |
} |
13 |
} |
輸出:
1 |
Counter result: 100000000 |
2 |
Time passed in ms: 6552 |
AtomicCounter的運行結果更好。最后,試試
Unsafe
原始的compareAndSwapLong
,看看它是否真的只有特權才能使用它?
01 |
class CASCounter implements Counter { |
02 |
private volatile long counter = 0 ; |
03 |
private Unsafe unsafe; |
04 |
private long offset; |
05 |
06 |
public CASCounter() throws Exception { |
07 |
unsafe = getUnsafe(); |
08 |
offset = unsafe.objectFieldOffset(CASCounter. class .getDeclaredField( "counter" )); |
09 |
} |
10 |
11 |
@Override |
12 |
public void increment() { |
13 |
long before = counter; |
14 |
while (!unsafe.compareAndSwapLong( this , offset, before, before + 1 )) { |
15 |
before = counter; |
16 |
} |
17 |
} |
18 |
19 |
@Override |
20 |
public long getCounter() { |
21 |
return counter; |
22 |
} |
23 |
} |
輸出:
1 |
Counter result: 100000000 |
2 |
Time passed in ms: 6454 |
看起來似乎等價於atomics。atomics使用Unsafe
?(是的)
實際上,這個例子很簡單,但它展示了Unsafe
的一些能力。
如我所說,CAS原語可以用來實現無鎖的數據結構。背后的原理很簡單:
- 有一些狀態
- 創建它的副本
- 修改它
- 執行CAS
- 如果失敗,重復嘗試
實際上,現實中比你現象的更難。存在着許多問題,如ABA問題、指令重排序等。
如果你真的感興趣,可以參考lock-free HashMap的精彩展示。
修改:給counter變量添加volatile
關鍵字,以避免無限循環的風險。
結論(Conclusion)
即使Unsafe
對應用程序很有用,但(建議)不要使用它。