前一段時間在研究juc
源碼的時候,發現在很多工具類中都調用了一個Unsafe
類中的方法,出於好奇就想要研究一下這個類到底有什么作用,於是先查閱了一些資料,一查不要緊,很多資料中對Unsafe的態度都是這樣的畫風:
其實看到這些說法也沒什么意外,畢竟Unsafe這個詞直譯過來就是“不安全的”,從名字里我們也大概能看來Java的開發者們對它有些不放心。但是作為一名極客,不能你說不安全我就不去研究了,畢竟只有了解一項技術的風險點,才能更好的避免出現這些問題嘛。
下面我們言歸正傳,先通過簡單的介紹來對Unsafe類有一個大致的了解。Unsafe類是一個位於sun.misc
包下的類,它提供了一些相對底層方法,能夠讓我們接觸到一些更接近操作系統底層的資源,如系統的內存資源、cpu指令等。而通過這些方法,我們能夠完成一些普通方法無法實現的功能,例如直接使用偏移地址操作對象、數組等等。但是在使用這些方法提供的便利的同時,也存在一些潛在的安全因素,例如對內存的錯誤操作可能會引起內存泄漏,嚴重時甚至可能引起jvm崩潰。因此在使用Unsafe前,我們必須要了解它的工作原理與各方法的應用場景,並且在此基礎上仍需要非常謹慎的操作,下面我們正式開始對Unsafe的學習。
Unsafe 基礎
首先我們來嘗試獲取一個Unsafe實例,如果按照new
的方式去創建對象,不好意思,編譯器會報錯提示你:
Unsafe() has private access in 'sun.misc.Unsafe'
查看Unsafe類的源碼,可以看到它被final
修飾不允許被繼承,並且構造函數為private
類型,即不允許我們手動調用構造方法進行實例化,只有在static
靜態代碼塊中,以單例的方式初始化了一個Unsafe對象:
public final class Unsafe {
private static final Unsafe theUnsafe;
...
private Unsafe() {
}
...
static {
theUnsafe = new Unsafe();
}
}
在Unsafe類中,提供了一個靜態方法getUnsafe
,看上去貌似可以用它來獲取Unsafe實例:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
但是如果我們直接調用這個靜態方法,會拋出異常:
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.cn.test.GetUnsafeTest.main(GetUnsafeTest.java:12)
這是因為在getUnsafe
方法中,會對調用者的classLoader
進行檢查,判斷當前類是否由Bootstrap classLoader
加載,如果不是的話那么就會拋出一個SecurityException
異常。也就是說,只有啟動類加載器加載的類才能夠調用Unsafe類中的方法,來防止這些方法在不可信的代碼中被調用。
那么,為什么要對Unsafe類進行這么謹慎的使用限制呢,說到底,還是因為它實現的功能過於底層,例如直接進行內存操作、繞過jvm的安全檢查創建對象等等,概括的來說,Unsafe類實現功能可以被分為下面8類:
創建實例
看到上面的這些功能,你是不是已經有些迫不及待想要試一試了。那么如果我們執意想要在自己的代碼中調用Unsafe類的方法,應該怎么獲取一個它的實例對象呢,答案是利用反射獲得Unsafe類中已經實例化完成的單例對象:
public static Unsafe getUnsafe() throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
//Field unsafeField = Unsafe.class.getDeclaredFields()[0]; //也可以這樣,作用相同
unsafeField.setAccessible(true);
Unsafe unsafe =(Unsafe) unsafeField.get(null);
return unsafe;
}
在獲取到Unsafe的實例對象后,我們就可以使用它為所欲為了,先來嘗試使用它對一個對象的屬性進行讀寫:
public void fieldTest(Unsafe unsafe) throws NoSuchFieldException {
User user=new User();
long fieldOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("age"));
System.out.println("offset:"+fieldOffset);
unsafe.putInt(user,fieldOffset,20);
System.out.println("age:"+unsafe.getInt(user,fieldOffset));
System.out.println("age:"+user.getAge());
}
運行代碼輸出如下,可以看到通過Unsafe類的objectFieldOffset
方法獲取了對象中字段的偏移地址,這個偏移地址不是內存中的絕對地址而是一個相對地址,之后再通過這個偏移地址對int
類型字段的屬性值進行了讀寫操作,通過結果也可以看到Unsafe的方法和類中的get
方法獲取到的值是相同的。
offset:12
age:20
age:20
在上面的例子中調用了Unsafe類的putInt
和getInt
方法,看一下源碼中的方法:
public native int getInt(Object o, long offset);
public native void putInt(Object o, long offset, int x);
先說作用,getInt
用於從對象的指定偏移地址處讀取一個int
,putInt
用於在對象指定偏移地址處寫入一個int
,並且即使類中的這個屬性是private
私有類型的,也可以對它進行讀寫。但是有細心的小伙伴可能發現了,這兩個方法相對於我們平常寫的普通方法,多了一個native
關鍵字修飾,並且沒有具體的方法邏輯,那么它是怎么實現的呢?
native方法
在java中,這類方法被稱為native
方法(Native Method
),簡單的說就是由java調用非java代碼的接口,被調用的方法是由非java 語言實現的,例如它可以由C或C++語言來實現,並編譯成DLL,然后直接供java進行調用。native
方法是通過JNI(Java Native Interface
)實現調用的,從 java1.1開始 JNI 標准就是java平台的一部分,它允許java代碼和其他語言的代碼進行交互。
Unsafe類中的很多基礎方法都屬於native
方法,那么為什么要使用native
方法呢?原因可以概括為以下幾點:
- 需要用到 java 中不具備的依賴於操作系統的特性,java在實現跨平台的同時要實現對底層的控制,需要借助其他語言發揮作用
- 對於其他語言已經完成的一些現成功能,可以使用java直接調用
- 程序對時間敏感或對性能要求非常高時,有必要使用更加底層的語言,例如C/C++甚至是匯編
在juc
包的很多並發工具類在實現並發機制時,都調用了native
方法,通過它們打破了java運行時的界限,能夠接觸到操作系統底層的某些功能。對於同一個native
方法,不同的操作系統可能會通過不同的方式來實現,但是對於使用者來說是透明的,最終都會得到相同的結果,至於java如何實現的通過JNI調用其他語言的代碼,不是本文的重點,會在后續的文章中具體學習。
Unsafe 應用
在對Unsafe的基礎有了一定了解后,我們來看一下它的基本應用。由於篇幅有限,不能對所有方法進行介紹,如果大家有學習的需要,可以下載openJDK的源碼進行學習。
1、內存操作
如果你是一個寫過c
或者c++
的程序員,一定對內存操作不會陌生,而在java中是不允許直接對內存進行操作的,對象內存的分配和回收都是由jvm
自己實現的。但是在Unsafe中,提供的下列接口可以直接進行內存操作:
//分配新的本地空間
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);
使用下面的代碼進行測試:
private void memoryTest() {
int size = 4;
long addr = unsafe.allocateMemory(size);
long addr3 = unsafe.reallocateMemory(addr, size * 2);
System.out.println("addr: "+addr);
System.out.println("addr3: "+addr3);
try {
unsafe.setMemory(null,addr ,size,(byte)1);
for (int i = 0; i < 2; i++) {
unsafe.copyMemory(null,addr,null,addr3+size*i,4);
}
System.out.println(unsafe.getInt(addr));
System.out.println(unsafe.getLong(addr3));
}finally {
unsafe.freeMemory(addr);
unsafe.freeMemory(addr3);
}
}
先看結果輸出:
addr: 2433733895744
addr3: 2433733894944
16843009
72340172838076673
分析一下運行結果,首先使用allocateMemory
方法申請4字節長度的內存空間,在循環中調用setMemory
方法向每個字節寫入內容為byte
類型的1,當使用Unsafe調用getInt
方法時,因為一個int
型變量占4個字節,會一次性讀取4個字節,組成一個int
的值,對應的十進制結果為16843009,可以通過圖示理解這個過程:
在代碼中調用reallocateMemory
方法重新分配了一塊8字節長度的內存空間,通過比較addr
和addr3
可以看到和之前申請的內存地址是不同的。在代碼中的第二個for循環里,調用copyMemory
方法進行了兩次內存的拷貝,每次拷貝內存地址addr
開始的4個字節,分別拷貝到以addr3
和addr3+4
開始的內存空間上:
拷貝完成后,使用getLong
方法一次性讀取8個字節,得到long
類型的值為72340172838076673。
需要注意,通過這種方式分配的內存屬於堆外內存,是無法進行垃圾回收的,需要我們把這些內存當做一種資源去手動調用freeMemory
方法進行釋放,否則會產生內存泄漏。通用的操作內存方式是在try
中執行對內存的操作,最終在finally
塊中進行內存的釋放。
2、內存屏障
在介紹內存屏障前,需要知道編譯器和CPU會在保證程序輸出結果一致的情況下,會對代碼進行重排序,從指令優化角度提升性能。而指令重排序可能會帶來一個不好的結果,導致CPU的高速緩存和內存中數據的不一致,而內存屏障(Memory Barrier
)就是通過組織屏障兩邊的指令重排序從而避免編譯器和硬件的不正確優化情況。
在硬件層面上,內存屏障是CPU為了防止代碼進行重排序而提供的指令,不同的硬件平台上實現內存屏障的方法可能並不相同。在java8中,引入了3個內存屏障的函數,它屏蔽了操作系統底層的差異,允許在代碼中定義、並統一由jvm來生成內存屏障指令,來實現內存屏障的功能。Unsafe中提供了下面三個內存屏障相關方法:
//禁止讀操作重排序
public native void loadFence();
//禁止寫操作重排序
public native void storeFence();
//禁止讀、寫操作重排序
public native void fullFence();
內存屏障可以看做對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作。以loadFence
方法為例,它會禁止讀操作重排序,保證在這個屏障之前的所有讀操作都已經完成,並且將緩存數據設為無效,重新從主存中進行加載。
看到這估計很多小伙伴們會想到volatile
關鍵字了,如果在字段上添加了volatile
關鍵字,就能夠實現字段在多線程下的可見性。基於讀內存屏障,我們也能實現相同的功能。下面定義一個線程方法,在線程中去修改flag
標志位,注意這里的flag
是沒有被volatile
修飾的:
@Getter
class ChangeThread implements Runnable{
/**volatile**/ boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}
在主線程的while
循環中,加入內存屏障,測試是否能夠感知到flag
的修改變化:
public static void main(String[] args){
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.isFlag();
unsafe.loadFence(); //加入讀內存屏障
if (flag){
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}
運行結果:
subThread change flag to:false
detected flag changed
main thread end
而如果刪掉上面代碼中的loadFence
方法,那么主線程將無法感知到flag
發生的變化,會一直在while
中循環。可以用圖來表示上面的過程:
了解java內存模型(JMM
)的小伙伴們應該清楚,運行中的線程不是直接讀取主內存中的變量的,只能操作自己工作內存中的變量,然后同步到主內存中,並且線程的工作內存是不能共享的。上面的圖中的流程就是子線程借助於主內存,將修改后的結果同步給了主線程,進而修改主線程中的工作空間,跳出循環。
3、對象操作
a、對象成員屬性的內存偏移量獲取,以及字段屬性值的修改,在上面的例子中我們已經測試過了。除了前面的putInt
、getInt
方法外,Unsafe提供了全部8種基礎數據類型以及Object
的put
和get
方法,並且所有的put
方法都可以越過訪問權限,直接修改內存中的數據。閱讀openJDK源碼中的注釋發現,基礎數據類型和Object
的讀寫稍有不同,基礎數據類型是直接操作的屬性值(value
),而Object
的操作則是基於引用值(reference value
)。下面是Object
的讀寫方法:
//在對象的指定偏移地址獲取一個對象引用
public native Object getObject(Object o, long offset);
//在對象指定偏移地址寫入一個對象引用
public native void putObject(Object o, long offset, Object x);
除了對象屬性的普通讀寫外,Unsafe還提供了volatile讀寫和有序寫入方法。volatile
讀寫方法的覆蓋范圍與普通讀寫相同,包含了全部基礎數據類型和Object
類型,以int
類型為例:
//在對象的指定偏移地址處讀取一個int值,支持volatile load語義
public native int getIntVolatile(Object o, long offset);
//在對象指定偏移地址處寫入一個int,支持volatile store語義
public native void putIntVolatile(Object o, long offset, int x);
相對於普通讀寫來說,volatile
讀寫具有更高的成本,因為它需要保證可見性和有序性。在執行get
操作時,會強制從主存中獲取屬性值,在使用put
方法設置屬性值時,會強制將值更新到主存中,從而保證這些變更對其他線程是可見的。
有序寫入的方法有以下三個:
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
有序寫入的成本相對volatile
較低,因為它只保證寫入時的有序性,而不保證可見性,也就是一個線程寫入的值不能保證其他線程立即可見。為了解決這里的差異性,需要對內存屏障的知識點再進一步進行補充,首先需要了解兩個指令的概念:
Load
:將主內存中的數據拷貝到處理器的緩存中Store
:將處理器緩存的數據刷新到主內存中
順序寫入與volatile
寫入的差別在於,在順序寫時加入的內存屏障類型為StoreStore
類型,而在volatile
寫入時加入的內存屏障是StoreLoad
類型,如下圖所示:
在有序寫入方法中,使用的是StoreStore
屏障,該屏障確保Store1
立刻刷新數據到內存,這一操作先於Store2
以及后續的存儲指令操作。而在volatile
寫入中,使用的是StoreLoad
屏障,該屏障確保Store1
立刻刷新數據到內存,這一操作先於Load2
及后續的裝載指令,並且,StoreLoad
屏障會使該屏障之前的所有內存訪問指令,包括存儲指令和訪問指令全部完成之后,才執行該屏障之后的內存訪問指令。
綜上所述,在上面的三類寫入方法中,在寫入效率方面,按照put
、putOrder
、putVolatile
的順序效率逐漸降低,
b、使用Unsafe的allocateInstance
方法,允許我們使用非常規的方式進行對象的實例化,首先定義一個實體類,並且在構造函數中對其成員變量進行賦值操作:
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
分別基於構造函數、反射以及Unsafe方法的不同方式創建對象進行比較:
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
打印結果分別為1、1、0,說明通過allocateInstance
方法創建對象過程中,不會調用類的構造方法。使用這種方式創建對象時,只用到了Class
對象,所以說如果想要跳過對象的初始化階段或者跳過構造器的安全檢查,就可以使用這種方法。在上面的例子中,如果將A類的構造函數改為private
類型,將無法通過構造函數和反射創建對象,但allocateInstance
方法仍然有效。
4、數組操作
在Unsafe中,可以使用arrayBaseOffset
方法可以獲取數組中第一個元素的偏移地址,使用arrayIndexScale
方法可以獲取數組中元素間的偏移地址增量。使用下面的代碼進行測試:
private void arrayTest() {
String[] array=new String[]{"str1str1str","str2","str3"};
int baseOffset = unsafe.arrayBaseOffset(String[].class);
System.out.println(baseOffset);
int scale = unsafe.arrayIndexScale(String[].class);
System.out.println(scale);
for (int i = 0; i < array.length; i++) {
int offset=baseOffset+scale*i;
System.out.println(offset+" : "+unsafe.getObject(array,offset));
}
}
上面代碼的輸出結果為:
16
4
16 : str1str1str
20 : str2
24 : str3
通過配合使用數組偏移首地址和各元素間偏移地址的增量,可以方便的定位到數組中的元素在內存中的位置,進而通過getObject
方法直接獲取任意位置的數組元素。需要說明的是,arrayIndexScale
獲取的並不是數組中元素占用的大小,而是地址的增量,按照openJDK中的注釋,可以將它翻譯為元素尋址的轉換因子(scale factor for addressing elements
)。在上面的例子中,第一個字符串長度為11字節,但其地址增量仍然為4字節。
那么,基於這兩個值是如何實現的尋址和數組元素的訪問呢,這里需要借助一點在前面的文章中講過的Java對象內存布局的知識,先把上面例子中的String數組對象的內存布局畫出來,就很方便大家理解了:
在String數組對象中,對象頭包含3部分,mark word
標記字占用8字節,klass point
類型指針占用4字節,數組對象特有的數組長度部分占用4字節,總共占用了16字節。第一個String的引用類型相對於對象的首地址的偏移量是就16,之后每個元素在這個基礎上加4,正好對應了我們上面代碼中的尋址過程,之后再使用前面說過的getObject
方法,通過數組對象可以獲得對象在堆中的首地址,再配合對象中變量的偏移量,就能獲得每一個變量的引用。
5、CAS操作
在juc
包的並發工具類中大量地使用了CAS操作,像在前面介紹synchronized
和AQS
的文章中也多次提到了CAS,其作為樂觀鎖在並發工具類中廣泛發揮了作用。在Unsafe類中,提供了compareAndSwapObject
、compareAndSwapInt
、compareAndSwapLong
方法來實現的對Object
、int
、long
類型的CAS操作。以compareAndSwapInt
方法為例:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
參數中o
為需要更新的對象,offset
是對象o
中整形字段的偏移量,如果這個字段的值與expected
相同,則將字段的值設為x
這個新值,並且此更新是不可被中斷的,也就是一個原子操作。下面是一個使用compareAndSwapInt
的例子:
private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}
private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
運行代碼會依次輸出:
1 2 3 4 5 6 7 8 9
在上面的例子中,使用兩個線程去修改int
型屬性a
的值,並且只有在a
的值等於傳入的參數x
減一時,才會將a
的值變為x
,也就是實現對a
的加一的操作。流程如下所示:
需要注意的是,在調用compareAndSwapInt
方法后,會直接返回true
或false
的修改結果,因此需要我們在代碼中手動添加自旋的邏輯。在AtomicInteger
類的設計中,也是采用了將compareAndSwapInt
的結果作為循環條件,直至修改成功才退出死循環的方式來實現的原子性的自增操作。
6、線程調度
Unsafe類中提供了park
、unpark
、monitorEnter
、monitorExit
、tryMonitorEnter
方法進行線程調度,在前面介紹AQS的文章中我們提到過使用LockSupport
掛起或喚醒指定線程,看一下LockSupport
的源碼,可以看到它也是調用的Unsafe類中的方法:
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
LockSupport的park
方法調用了Unsafe的park
方法來阻塞當前線程,此方法將線程阻塞后就不會繼續往后執行,直到有其他線程調用unpark
方法喚醒當前線程。下面的例子對Unsafe的這兩個方法進行測試:
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("subThread try to unpark mainThread");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("park main mainThread");
unsafe.park(false,0L);
System.out.println("unpark mainThread success");
}
程序輸出為:
park main mainThread
subThread try to unpark mainThread
unpark mainThread success
程序運行的流程也比較容易看懂,子線程開始運行后先進行睡眠,確保主線程能夠調用park
方法阻塞自己,子線程在睡眠5秒后,調用unpark
方法喚醒主線程,使主線程能繼續向下執行。整個流程如下圖所示:
此外,Unsafe源碼中monitor
相關的三個方法已經被標記為deprecated
,不建議被使用:
//獲得對象鎖
@Deprecated
public native void monitorEnter(Object var1);
//釋放對象鎖
@Deprecated
public native void monitorExit(Object var1);
//嘗試獲得對象鎖
@Deprecated
public native boolean tryMonitorEnter(Object var1);
monitorEnter
方法用於獲得對象鎖,monitorExit
用於釋放對象鎖,如果對一個沒有被monitorEnter
加鎖的對象執行此方法,會拋出IllegalMonitorStateException
異常。tryMonitorEnter
方法嘗試獲取對象鎖,如果成功則返回true
,反之返回false
。
7、Class操作
Unsafe對Class
的相關操作主要包括類加載和靜態變量的操作方法。
a、靜態屬性讀取相關的方法:
//獲取靜態屬性的偏移量
public native long staticFieldOffset(Field f);
//獲取靜態屬性的對象指針
public native Object staticFieldBase(Field f);
//判斷類是否需要實例化(用於獲取類的靜態屬性前進行檢測)
public native boolean shouldBeInitialized(Class<?> c);
創建一個包含靜態屬性的類,進行測試:
@Data
public class User {
public static String name="Hydra";
int age;
}
private void staticTest() throws Exception {
User user=new User();
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField("name");
long fieldOffset = unsafe.staticFieldOffset(sexField);
Object fieldBase = unsafe.staticFieldBase(sexField);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}
運行結果:
false
Hydra
在Unsafe的對象操作中,我們學習了通過objectFieldOffset
方法獲取對象屬性偏移量並基於它對變量的值進行存取,但是它不適用於類中的靜態屬性,這時候就需要使用staticFieldOffset
方法。在上面的代碼中,只有在獲取Field
對象的過程中依賴到了Class
,而獲取靜態變量的屬性時不再依賴於Class
。
在上面的代碼中首先創建一個User
對象,這是因為如果一個類沒有被實例化,那么它的靜態屬性也不會被初始化,最后獲取的字段屬性將是null
。所以在獲取靜態屬性前,需要調用shouldBeInitialized
方法,判斷在獲取前是否需要初始化這個類。如果刪除創建User對象的語句,運行結果會變為:
true
null
b、使用defineClass
方法允許程序在運行時動態地創建一個類,方法定義如下:
public native Class<?> defineClass(String name, byte[] b, int off, int len,
ClassLoader loader,ProtectionDomain protectionDomain);
在實際使用過程中,可以只傳入字節數組、起始字節的下標以及讀取的字節長度,默認情況下,類加載器(ClassLoader
)和保護域(ProtectionDomain
)來源於調用此方法的實例。下面的例子中實現了反編譯生成后的class文件的功能:
private static void defineTest() {
String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] content=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}
在上面的代碼中,首先讀取了一個class
文件並通過文件流將它轉化為字節數組,之后使用defineClass
方法動態的創建了一個類,並在后續完成了它的實例化工作,流程如下圖所示,並且通過這種方式創建的類,會跳過JVM的所有安全檢查。
除了defineClass
方法外,Unsafe還提供了一個defineAnonymousClass
方法:
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
使用該方法可以用來動態的創建一個匿名類,在Lambda
表達式中就是使用ASM動態生成字節碼,然后利用該方法定義實現相應的函數式接口的匿名類。在jdk15發布的新特性中,在隱藏類(Hidden classes
)一條中,指出將在未來的版本中棄用Unsafe的defineAnonymousClass
方法。
8、系統信息
Unsafe中提供的addressSize
和pageSize
方法用於獲取系統信息,調用addressSize
方法會返回系統指針的大小,如果在64位系統下默認會返回8,而32位系統則會返回4。調用pageSize方法會返回內存頁的大小,值為2的整數冪。使用下面的代碼可以直接進行打印:
private void systemTest() {
System.out.println(unsafe.addressSize());
System.out.println(unsafe.pageSize());
}
執行結果:
8
4096
這兩個方法的應用場景比較少,在java.nio.Bits
類中,在使用pageCount
計算所需的內存頁的數量時,調用了pageSize
方法獲取內存頁的大小。另外,在使用copySwapMemory
方法拷貝內存時,調用了addressSize
方法,檢測32位系統的情況。
總結
在本文中,我們首先介紹了Unsafe的基本概念、工作原理,並在此基礎上,對它的API進行了說明與實踐。相信大家通過這一過程,能夠發現Unsafe在某些場景下,確實能夠為我們提供編程中的便利。但是回到開頭的話題,在使用這些便利時,確實存在着一些安全上的隱患,在我看來,一項技術具有不安全因素並不可怕,可怕的是它在使用過程中被濫用。盡管之前有傳言說會在java9中移除Unsafe類,不過它還是照樣已經存活到了jdk16,按照存在即合理的邏輯,只要使用得當,它還是能給我們帶來不少的幫助,因此最后還是建議大家,在使用Unsafe的過程中一定要做到使用謹慎使用、避免濫用。
如果文章對您有所幫助,歡迎關注公眾號 碼農參上