深入理解Unsafe類
Unsafe 類位于 sun.misc 包中,sun.misc 包本身在工作中就是個很少被用到的包。在 Java 的發(fā)展中,sun.misc 包是 Sun 公司早年的內(nèi)部工具包,提供了很多底層操作系統(tǒng)級別的方法調(diào)用,擁有很大的權(quán)限。然而,大多數(shù)開發(fā)手冊都不推薦使用 sun.misc 包,因為直接使用 sun.misc 包下的類,可能會帶來安全風險和不可控性。
還記得 Java 和 C 語言相比有什么優(yōu)勢嗎?
Java 中是沒有指針的。在程序中維護 C 語言指針的經(jīng)歷一定曾讓你焦頭爛額,而 Java 語言中避免了這種指針操作,這就使得編碼的安全性、效率得到大大地提升。
現(xiàn)在,Java 通過 Unsafe 保留了對指針的操作能力。這看上去有點前后矛盾,好像說不要指針的是 Java,說要指針的也是 Java。然而,那么多優(yōu)秀框架底層都用了 Unsafe,那自然是有它適合的場景。
接下來,我們就來講講 Unsafe 類的創(chuàng)建和它的兩個常見的應(yīng)用場景。
創(chuàng)建 Unsafe
我們先來查看一下 Unsafe 的源碼。
public finalclass Unsafe {
privatestaticfinal Unsafe theUnsafe;
......
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
thrownew SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
getUnsafe 似乎可以直接獲取一個 Unsafe 對象,然而實際調(diào)用后,getUnsafe 方法一定會拋出 SecurityException 異常。這是因為 isSystemDomainLoader 方法會對調(diào)用者的 ClassLoader 進行檢查,如果調(diào)用者的 ClassLoader 不是 BootStrap ClassLoader,調(diào)用者就會拋出 SecurityException 異常。
也就是說,只有 JDK 自己的類才可以使用 getUnsafe 來獲取 Unsafe 實例,我們工程師自己的方法是沒有權(quán)限調(diào)用 getUnsafe 方法的。
這種情況下,我們?nèi)绾潍@取 Unsafe 實例呢?這里有兩個方案,我們來一起看一下。
方案一,利用反射。在 Unsafe 的源碼中,有一個 Unsafe 類型的成員變量——theUnsafe,我們可以通過反射來直接獲取這個變量。
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
因為 theUnsafe 是 private 修飾的,所以我們可以直接用 setAccessible 強制打開訪問權(quán)限,這樣就繞開了層層封鎖,可以直接獲取 Unsafe 對象了。
方案二,我們可以強制把我們的類放入 BootStrap ClassLoader 的 classpath。JDK 提供了-Xbootclasspath/a 命令允許我們把自己寫的類加入 BootStrap ClassLoader 路徑。這樣就可以直接通過上面的 getUnsafe 方法獲取 Unsafe 對象了。
千辛萬苦創(chuàng)建了 Unsafe 之后,我們來繼續(xù)看看 Unsafe 的使用場景。由于 Unsafe 的主要功能是管理內(nèi)存,因此我們就來一起看看,Unsafe 是如何實現(xiàn)內(nèi)存操作和內(nèi)存屏障的。
內(nèi)存操作
JVM 強大的一點功能是內(nèi)存的自動管理,可以實現(xiàn)對象的自動回收。然而,一些特殊場景,如 NIO 的直接內(nèi)存,并沒有走 JVM 的自動內(nèi)存管理。Unsafe 允許我們像 C 語言那樣使用指針直接操作內(nèi)存,它的 API 如下:
public native long allocateMemory(long bytes);
public native long reallocateMemory(long address, long bytes);
public native void setMemory(Object o, long offset, long bytes, byte value);
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
public native void freeMemory(long address);
其中,allocateMemory 是分配內(nèi)存空間,reallocateMemory 方法可以重新調(diào)整內(nèi)存空間大小,setMemory 可以設(shè)置內(nèi)存的值,copyMemory 和 freeMemory 分別是拷貝和清除。這些方法和 C 語言幾乎是對應(yīng)的。
我們來看一個具體的例子吧。運行這段代碼,會輸出什么呢?
long addr = unsafe.allocateMemory(4);
unsafe.setMemory(null,addr ,size,(byte)1);
System.out.println(unsafe.getInt(addr));
輸出的是 16843009。為什么會這樣呢?
首先,unsafe.allocateMemory(4) 分配了一個 4 字節(jié)的空間,setMemory 則以 addr 為開始,以 addr+size 為結(jié)尾,向每個字節(jié)分別寫入 1,這時候的內(nèi)存空間是這樣的:
圖片
getInt 方法會把結(jié)果轉(zhuǎn)成 10 進制并返回,也就是 16843009。
需要注意的是,allocateMemory 分配的是堆外內(nèi)存,是沒有辦法自動 GC 的,此時我們只能手動調(diào)用 freeMemory 方法才可以釋放內(nèi)存。對于上面的代碼,我們可以在 finally 語句塊中調(diào)用 freeMemory 來釋放 addr。
finally {
unsafe.freeMemory(addr);
}
使用堆外內(nèi)存有什么好處呢?
第一個顯而易見的好處是減少了 GC。數(shù)據(jù)放在堆外內(nèi)存,就和 GC 毫無關(guān)系了。
其次,提升了 I/O 操作的性能。我們讀取文件或網(wǎng)絡(luò)數(shù)據(jù)的時候,不可避免地需要在操作系統(tǒng)內(nèi)存和 JVM 內(nèi)存之間拷貝數(shù)據(jù)。雖然拷貝數(shù)據(jù)的這個過程是透明的,但占用了一定時間,直接使用堆外內(nèi)存則減少了一次不必要的內(nèi)存復制工作,進而提升了 I/O 整體性能。我們熟知的 DirectByteBuffer 底層就是基于 Unsafe 實現(xiàn)的。
內(nèi)存屏障
接下來,我們再來看看 Unsafe 類在內(nèi)存屏障場景中的應(yīng)用。
說到內(nèi)存屏障,我們就不得不提“指令重排序”了。在多線程中,“指令重排序”是一個經(jīng)常被提到的概念,簡單來說,就是操作系統(tǒng)在保證輸出結(jié)果正確的情況下,對你的代碼執(zhí)行順序進行調(diào)整,以提升系統(tǒng)執(zhí)行性能。“指令重排序”的弊端在于它可能導致 CPU Cache 和內(nèi)存中的數(shù)據(jù)不一致。
而內(nèi)存屏障是制止重排序的指令,當然“指令重排序”的目標是為了優(yōu)化執(zhí)行性能,如果二話不說直接制止“指令重排序”也是不推薦的。只有當“指令重排序”影響正確結(jié)果的情況下,我們才去制止它。Unsafe 提供了下面 3 個內(nèi)存屏障 API,你看一下:
public native void loadFence();
public native void storeFence();
public native void fullFence();
從名字上看,loadFence 作用于 JVM 的 Load 匯編指令,storeFence 作用于 JVM 的 Store 匯編指令,而 fullFence 同時會對 Load 和 Store 生效。對 JVM 匯編指令沒有了解的同學可能認為 Load 就是讀操作,Store 就是寫操作。
對于這 3 個 API,我們用個形象的比喻來說明一下它們的作用吧。假設(shè)你要去做核酸檢測,此時排起了長隊,不時還出現(xiàn)插隊現(xiàn)象,讓人不堪其擾。于是,你在隊伍中堆起了一堵高大的墻,墻兩邊的人依然會出現(xiàn)插隊現(xiàn)象,但墻一邊的人無法到達另一邊,這就是屏障的作用。
換成更專業(yè)的表述就是屏障是一個同步點,使得同步點前的操作必然在同步點后的操作執(zhí)行,同時屏障會使得 CPU Cache 中的數(shù)據(jù)失效,強制指令走內(nèi)存讀取數(shù)據(jù)。Java 中的 StampedLock 讀寫鎖,就是使用了內(nèi)存屏障來實現(xiàn)的。
總結(jié)
我們介紹了 Unsafe 的基本概念和創(chuàng)建方法,并講了內(nèi)存操作和內(nèi)存屏障兩個場景。通過這節(jié)課的學習,相信大家可以發(fā)現(xiàn),Unsafe 能給我們帶來實實在在的好處。當然,Unsafe 如同它的名稱一樣,存在不安全的隱患。然而,直到現(xiàn)在 Unsafe 依然存在。這說明,在正確使用的情況下,Unsafe 一定是利大于弊的。
最后講一句,不到萬不得已,不要輕易使用 Unsafe。我們講解 Unsafe 是為了讓大家對底層原理的理解更加深入透徹,至于在生產(chǎn)中應(yīng)用 Unsafe,還要三思而后行。