前言
Java是一個(gè)安全的編程語(yǔ)言,它能最大程度的防止程序員犯一些低級(jí)的錯(cuò)誤(大部分是和內(nèi)存管理有關(guān)的)。但凡是不是絕對(duì)的,使用Unsafe程序員就可以操作內(nèi)存,因此可能帶來一個(gè)安全隱患。
這篇文章是就快速學(xué)習(xí)下sun.misc.Unsafe
的公共API和一些有趣的使用例子。
Unsafe 實(shí)例化
在使用Unsafe之前我們需要先實(shí)例化它。但我們不能通過像Unsafe unsafe = new Unsafe()
這種簡(jiǎn)單的方式來實(shí)現(xiàn)Unsafe的實(shí)例化,這是由于Unsafe的構(gòu)造方法是私有的。Unsafe有一個(gè)靜態(tài)的getUnsafe()方法,但是如果天真的以為調(diào)用該方法就可以的話,那你將遇到一個(gè)SecurityException
異常,這是由于該方法只能在被信任的代碼中調(diào)用。
public static Unsafe getUnsafe() {
Class cc = sun.reflect.Reflection.getCallerClass(2);
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
return theUnsafe;
}
那Java是如何判斷我們的代碼是否是受信的呢?它就是通過判斷加載我們代碼的類加載器是否是根類加載器。
我們可是通過這種方法將我們自己的代碼變?yōu)槭苄诺模褂胘vm參數(shù)bootclasspath
。如下所示:
java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient
但這種方式太難了
Unsafe類內(nèi)部有一個(gè)名為theUnsafe
的私有實(shí)例變量,我們可以通過反射來獲取該實(shí)例變量。
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
注意: 忽略你的IDE提示. 例如, eclipse可能會(huì)報(bào)這樣的錯(cuò)誤”Access restriction…” 單如果你運(yùn)行你的代碼,會(huì)發(fā)現(xiàn)一切正常。如果還是還是提示錯(cuò)誤,你可以通過如下的方式關(guān)閉該錯(cuò)誤提示:
Preferences -> Java -> Compiler -> Errors/Warnings ->
Deprecated and restricted API -> Forbidden reference -> Warning
Unsafe API
類 sun.misc.Unsafe 由150個(gè)方法組成。事實(shí)上這些方法只有幾組是非常重要的用來操作不同的對(duì)象。下面我們就來看下這些方法中的一部分。
-
Info 僅僅是返回一個(gè)低級(jí)別的內(nèi)存相關(guān)的信息
- addressSize
- pageSize
-
Objects. 提供操作對(duì)象和對(duì)象字段的方法
- allocateInstance
- objectFieldOffset
-
Classes. 提供針對(duì)類和類的靜態(tài)字段操作的方法
- staticFieldOffset
- defineClass
- defineAnonymousClass
- ensureClassInitialized
-
Arrays. 數(shù)組操作
- arrayBaseOffset
- arrayIndexScale
- Synchronization. 低級(jí)別的同步原語(yǔ)
- monitorEnter
- tryMonitorEnter
- monitorExit
- compareAndSwapInt
- putOrderedInt
- Memory. 直接訪問內(nèi)存的方法
- allocateMemory
- copyMemory
- freeMemory
- getAddress
- getInt
- putInt
有趣的使用case
跳過構(gòu)造初始化
allocateInstance方法可能是有用的,當(dāng)你需要在構(gòu)造函數(shù)中跳過對(duì)象初始化階段或繞過安全檢查又或者你想要實(shí)例化哪些沒有提供公共構(gòu)造函數(shù)的類時(shí)就可以使用該方法。考慮下面的類:
class A {
private long a; // not initialized value
public A() {
this.a = 1; // initialization
}
public long a() { return this.a; }
}
通過構(gòu)造函數(shù),反射,Unsafe分別來實(shí)例化該類結(jié)果是不同的:
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
思考一下這些確保對(duì)Singletons模式的影響。
內(nèi)存泄露
對(duì)C程序員來說這中情況是很常見的。
思考一下一些簡(jiǎn)單的類是如何堅(jiān)持訪問規(guī)則的:
class Guard {
private int ACCESS_ALLOWED = 1;
public boolean giveAccess() {
return 42 == ACCESS_ALLOWED;
}
}
客戶端代碼是非常安全的,調(diào)用giveAccess()檢查訪問規(guī)則。不幸的是對(duì)所有的客戶端代碼,它總是返回false。只有特權(quán)用戶在某種程度上可以改變ACCESS_ALLOWED常量并且獲得訪問權(quán)限。
事實(shí)上,這不是真的。這是證明它的代碼:
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
現(xiàn)在所有的客戶端都沒有訪問限制了。
事實(shí)上同樣的功能也可以通過反射來實(shí)現(xiàn)。但有趣的是, 通過上面的方式我們修改任何對(duì)象,即使我們沒有持有對(duì)象的引用。
舉個(gè)例子, 在內(nèi)存中有另外的一個(gè)Guard對(duì)象,并且地址緊挨著當(dāng)前對(duì)象的地址,我們就可以通過下面的代碼來修改該對(duì)象的ACCESS_ALLOWED
字段的值。
unsafe.putInt(guard, 16 + unsafe.objectFieldOffset(f), 42); // memory corruption
注意,我們沒有使用任何指向該對(duì)象的引用,16是Guard對(duì)象在32位架構(gòu)上的大小。我們也可以通過sizeOf
方法來計(jì)算Guard對(duì)象的大小。
sizeOf
使用objectFieldOffset
方法我們可以實(shí)現(xiàn)C風(fēng)格的sizeof方法。下面的方法實(shí)現(xiàn)返回對(duì)象的表面上的大小
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
}
算法邏輯如下:收集所有包括父類在內(nèi)的非靜態(tài)字段,獲得每個(gè)字段的偏移量,發(fā)現(xiàn)最大并添加填充。也許,我錯(cuò)過了一些東西,但是概念是明確的。
更簡(jiǎn)單的sizeof方法實(shí)現(xiàn)邏輯是:我們只讀取該對(duì)象對(duì)應(yīng)的class對(duì)象中關(guān)于大小的字段值。在JVM 1.7 32 位
版本上該表示大小的字段偏移量是12。
public static long sizeOf(Object object){
return getUnsafe().getAddress(
normalize(getUnsafe().getInt(object, 4L)) + 12L);
}
normalize
是一個(gè)將有符號(hào)的int類型轉(zhuǎn)為無符號(hào)的long類型的方法。
private static long normalize(int value) {
if(value >= 0) return value;
return (~0L >>> 32) & value;
}
太棒了,這個(gè)方法返回的結(jié)果和我們之前的sizeof函數(shù)是相同的。
but it requires specifyng agent option in your JVM.
事實(shí)上,對(duì)于合適的,安全的,準(zhǔn)確的sizeof函數(shù)最好使用java.lang.instrument
包,但它需要特殊的JVM參數(shù)。
淺拷貝
在實(shí)現(xiàn)了計(jì)算對(duì)象淺層大小的基礎(chǔ)上,我們可以非常容易的添加對(duì)象的拷貝方法。標(biāo)準(zhǔn)的辦法需要修改我們的代碼和Cloneable。或者你可以實(shí)現(xiàn)自定義的對(duì)象拷貝函數(shù),但它不會(huì)變?yōu)橥ㄓ玫暮瘮?shù)。
淺拷貝:
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
將對(duì)象轉(zhuǎn)為它在內(nèi)存中的地址或者從指定的地址內(nèi)容轉(zhuǎn)為對(duì)象。
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];
}
該拷貝函數(shù)可以用來拷貝任何類型的對(duì)象,因?yàn)閷?duì)象的大小是動(dòng)態(tài)計(jì)算的。
注意 在完成拷貝動(dòng)作后你需要將拷貝對(duì)象的類型強(qiáng)轉(zhuǎn)為目標(biāo)類型。
隱藏密碼
在Unsafe的直接內(nèi)存訪問方法使用case中有一個(gè)非常有趣的用法就是刪除內(nèi)存中不想要的對(duì)象。
大多數(shù)獲取用戶密碼的API方法的返回值不是byte[]就是char[],這是為什么呢?
這完全是出于安全原因, 因?yàn)槲覀兛梢栽诓恍枰鼈兊臅r(shí)候?qū)?shù)組元素置為失效。如果我們獲取的密碼是字符串類型,則密碼字符串是作為一個(gè)對(duì)象保存在內(nèi)存中的。要將該密碼字符串置為無效,我們只能講字符串引用職位null,但是該字符串的內(nèi)容任然存在內(nèi)存直到GC回收該對(duì)象后。
這個(gè)技巧在內(nèi)存創(chuàng)建一個(gè)假的大小相同字符串對(duì)象來替換原來的:
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); // ????????????
感覺安全了嗎?
其實(shí)該方法不是真的安全。想要真的安全我們可以通過反射API將字符串對(duì)象中的字符數(shù)組value
字段的值修改為null。
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] = '?';
}
多重繼承
在Java中本來是沒有多重集成的。除非我們可以將任意的類型轉(zhuǎn)為我們想要的任意類型。
long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
long strClassAddress = normalize(getUnsafe().getInt("", 4L));
getUnsafe().putAddress(intClassAddress + 36, strClassAddress);
這段代碼將String類添加到Integer的超類集合中,所以我們的強(qiáng)轉(zhuǎn)代碼是沒有運(yùn)行時(shí)異常的。
(String) (Object) (new Integer(666))
有個(gè)問題是我們需要先將要轉(zhuǎn)的對(duì)象轉(zhuǎn)為Object,然后再轉(zhuǎn)為我們想要的類型。這是為了欺騙編譯器。
動(dòng)態(tài)類
We can create classes in runtime, for example from compiled .class file. To perform that read class contents to byte array and pass it properly to defineClass method.
我們可以在運(yùn)行時(shí)創(chuàng)建類, 例如通過一個(gè)編譯好的class文件。將class文件的內(nèi)容讀入到字節(jié)數(shù)組中然后將該數(shù)組傳遞到合適的defineClass
方法中。
byte[] classContents = getClassContent();
Class c = getUnsafe().defineClass(
null, classContents, 0, classContents.length);
c.getMethod("a").invoke(c.newInstance(), null); // 1
讀取class文件內(nèi)如的代碼:
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;
}
該方式是非常有用的,如果你確實(shí)需要在運(yùn)行時(shí)動(dòng)態(tài)的創(chuàng)建類。比如生產(chǎn)代理類或切面類。
拋出一個(gè)異常
不喜歡受檢異常?這不是問題。
getUnsafe().throwException(new IOException());
該方法拋出一個(gè)受檢異常,但是你的代碼不需要強(qiáng)制捕獲該異常就像運(yùn)行時(shí)異常一樣。
快速序列化
這種使用方式更實(shí)用。
每個(gè)人都知道java標(biāo)準(zhǔn)的序列化的功能速度很慢而且它還需要類擁有公有的構(gòu)造函數(shù)。
外部序列化是更好的方式,但是需要定義針對(duì)待序列化類的schema。
非常流行的高性能序列化庫(kù),像kryo是有使用限制的,比如在內(nèi)存缺乏的環(huán)境就不合適。
但通過使用Unsafe類我們可以非常簡(jiǎn)單的實(shí)現(xiàn)完整的序列化功能。
序列化:
- 通過反射定義類的序列化。 這個(gè)可以只做一次。
- 通過Unsafe的
getLong
,getInt
,getObject
等方法獲取字段真實(shí)的值。 - 添加可以恢復(fù)該對(duì)象的標(biāo)識(shí)符。
- 將這些數(shù)據(jù)寫入到輸出
當(dāng)然也可以使用壓縮來節(jié)省空間。
反序列化:
- 創(chuàng)建一個(gè)序列化類的實(shí)例,可以通過方法
allocateInstance
。因?yàn)樵摲椒ú恍枰魏螛?gòu)造方法。 - 創(chuàng)建schama, 和序列化類似
- 從文件或輸入讀取或有的字段
- 使用
Unsafe
的putLong
,putInt
,putObject
等方法來填充對(duì)象。
Actually, there are much more details in correct inplementation, but intuition is clear.
事實(shí)上要正確實(shí)現(xiàn)序列化和反序列化需要注意很多細(xì)節(jié),但是思路是清晰的。
這種序列化方式是非常快的。
順便說一句,在 kryo
有許多使用Unsafe
的嘗試 http://code.google.com/p/kryo/issues/detail?id=75
大數(shù)組
如你所知Java數(shù)組長(zhǎng)度的最大值是Integer.MAX_VALUE
。使用直接內(nèi)存分配我們可以創(chuàng)建非常大的數(shù)組,該數(shù)組的大小只受限于堆的大小。
這里有一個(gè)SuperArray
的實(shí)現(xiàn):
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;
}
}
一個(gè)簡(jiǎn)單的用法:
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
事實(shí)上該技術(shù)使用了非堆內(nèi)存off-heap memory
,在 java.nio
包中也有使用。
通過這種方式分配的內(nèi)存不在堆上,并且不受GC管理。因此需要小心使用Unsafe.freeMemory()
。該方法不會(huì)做任何邊界檢查,因此任何不合法的訪問可能就會(huì)導(dǎo)致JVM奔潰。
這種使用方式對(duì)于數(shù)學(xué)計(jì)算是非常有用的,因?yàn)榇a可以操作非常大的數(shù)據(jù)數(shù)組。 同樣的編寫實(shí)時(shí)程序的程序員對(duì)此也非常感興趣,因?yàn)椴皇蹽C限制,就不會(huì)因?yàn)镚C導(dǎo)致非常大的停頓。
并發(fā)
關(guān)于并發(fā)編程使用Unsafe的只言片語(yǔ)。compareAndSwap
方法是原子的,可以用來實(shí)現(xiàn)高性能的無鎖化數(shù)據(jù)結(jié)構(gòu)。
舉個(gè)例子,多個(gè)線程并發(fā)的更新共享的對(duì)象這種場(chǎng)景:
首先我們定義一個(gè)簡(jiǎn)單的接口 Counter
:
interface Counter {
void increment();
long getCounter();
}
我們定義工作線程 CounterClient
, 它會(huì)使用 Counter
:
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();
}
}
}
這是測(cè)試代碼:
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));
第一個(gè)實(shí)現(xiàn)-沒有同步的計(jì)數(shù)器
class StupidCounter implements Counter {
private long counter = 0;
@Override
public void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
Output:
Counter result: 99542945
Time passed in ms: 679
速度很多,但是沒有對(duì)所有的線程進(jìn)行協(xié)調(diào)所以結(jié)果是錯(cuò)誤的。第二個(gè)版本,使用Java常見的同步方式來實(shí)現(xiàn)
class SyncCounter implements Counter {
private long counter = 0;
@Override
public synchronized void increment() {
counter++;
}
@Override
public long getCounter() {
return counter;
}
}
Output:
Counter result: 100000000
Time passed in ms: 10136
徹底的同步當(dāng)然會(huì)導(dǎo)致正確的結(jié)果。但是花費(fèi)的時(shí)間令人沮喪。讓我們?cè)囋?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;
}
}
Output:
Counter result: 100000000
Time passed in ms: 8065
結(jié)果依然是正確的,時(shí)間也短。那使用原子的類呢?
class AtomicCounter implements Counter {
AtomicLong counter = new AtomicLong(0);
@Override
public void increment() {
counter.incrementAndGet();
}
@Override
public long getCounter() {
return counter.get();
}
}
Output:
Counter result: 100000000
Time passed in ms: 6552
使用AtomicCounter
的效果更好一點(diǎn)。最后我們?cè)囋?code>Unsafe的原子方法compareAndSwapLong
看看是不是更進(jìn)一步。
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;
}
}
Output:
Counter result: 100000000
Time passed in ms: 6454
開起來和使用原子類是一樣的效果,難道原子類使用了Unsafe
?答案是YES。
事實(shí)上該例子非常簡(jiǎn)單但表現(xiàn)出了Unsafe
的強(qiáng)大功能。
就像前面提到的 CAS
原語(yǔ)可以用來實(shí)現(xiàn)高效的無鎖數(shù)據(jù)結(jié)構(gòu)。實(shí)現(xiàn)的原理很簡(jiǎn)單:
- 擁有一個(gè)狀態(tài)
- 創(chuàng)建一個(gè)它的副本
- 修改該副本
- 執(zhí)行 CAS 操作
- 如果失敗就重復(fù)執(zhí)行
事實(shí)上,在真實(shí)的環(huán)境它的實(shí)現(xiàn)難度超過你的想象,這其中有需要類似ABA,指令重排序這樣的問題。
如果你確實(shí)對(duì)此感興趣,你可以參考關(guān)于無鎖HashMap的精彩演示。
Bonus
Documentation for park method from Unsafe class contains longest English sentence I’ve ever seen:
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.
結(jié)論
盡管Unsafe有這么多有用的應(yīng)用,但是盡力不要使用。當(dāng)然了使用JDK中利用了Unsafe實(shí)現(xiàn)的類是可以的。或者你對(duì)你代碼功力非常自信。
參考地址
https://leokongwq.github.io/2016/12/31/java-magic-unsafe.html
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
如果大家喜歡我的文章,可以關(guān)注個(gè)人訂閱號(hào)。歡迎隨時(shí)留言、交流。如果想加入微信群的話一起討論的話,請(qǐng)加管理員簡(jiǎn)棧文化-小助手(lastpass4u),他會(huì)拉你們進(jìn)群。