一個名為不安全的類Unsafe

小高飛發表於2020-11-10

最近為了更加深入瞭解NIO的實現原理,學習NIO的原始碼時,遇到了一個問題。即在WindowsSelectorImpl中的

pollWrapper屬性,當我點進去檢視它的PollArrayWrapper型別時,發現它和AllocatedNativeObject型別有關,而AllocatedNativeObject繼承了NativeObject類,隨著又發現了NativeObject是基於一個Unsafe類實現的。不安全的類????

 

Unsafe

Unsafe,顧名思義,它真是一個不安全的類,那它為什麼是不安全的呢?這就要從Unsafe類的功能說起。

學過C#的就可以知道,C#和Java的一個重要區別就是:C#可以直接操作一塊記憶體區域,如自己申請記憶體和釋放,而在Java中這是做不到的。而Unsafe類就可以讓我們在Java中像C#一樣去直接操作一塊記憶體區域,正因為Unsafe類可以直接操作記憶體,意味著其速度更快,在高併發的條件之下能夠很好地提高效率,所以java中很多併發框架,如Netty,都使用了Unsafe類。

雖然,Unsafe可以提高執行速度,但是因為Java本身是不支援自己直接操作記憶體的,這就意味著Unsafe類所做的操作不受jvm管理的,所以不會被GC(垃圾回收),需要我們手動GC,稍有不慎就會出現記憶體洩漏問題。且Unsafe的不少方法中必須提供原始地址(記憶體地址)和被替換物件的地址,偏移量要自己計算,一旦出現問題就是JVM崩潰級別的異常,會導致整個JVM例項崩潰。這就是為什麼Unsafe被稱為不安全的原因。Unsafe可以讓你全力踩油門,提高自己的速度,但是它會讓你的方向盤更難握穩,一不小心就可能導致車毀人亡。

原始碼檢視

初始化

因為Unsafe的構造方法是private型別的,所以無法通過new方式例項化獲取,只能通過它的getUnsafe()方法獲取。又因為Unsafe是直接操作記憶體的,為了安全起見,Java的開發人員為Unsafe的獲取設定了限制,所以想要獲取它只能通過Java的反射機制來獲取。

@CallerSensitive
public static Unsafe getUnsafe() {
    //通過getCallerClass方法獲取Unsafe類
    Class var0 = Reflection.getCallerClass();
    //如過該var0類不是啟動類載入器(Bootstrap),則丟擲異常
    //正因為該判斷,所以Unsafe只能通過反射獲取
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}
  • Reflection.getCallerClass():可以返回撥用類或Reflection類,或者層層上傳

  • VM.isSystemDomainLoader(ClassLoader var0):判斷該類載入器是否是啟動類載入器(Bootstrap)。

  • @CallerSensitive:為了防止黑客通過雙重反射來提升許可權,所以所有跟反射相關的介面方法都標註上CallerSensitive

所以使用下面的方式是獲取不了Unsafe類的:

//使用這樣的方式獲取會丟擲異常,因為是通過系統類載入器載入(AppClassLoader)
public class Test {
    public static void main(String[] args) {
        Unsafe unsafe = Unsafe.getUnsafe();
    }
}

那怎麼才用使用啟動類載入Unsafe類並獲取它呢?在Unsafe類的最下面的static程式碼塊中有這樣一段程式碼:

private static final Unsafe theUnsafe;
//.....
static {
    registerNatives();
    Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
    theUnsafe = new Unsafe();
    //......
}

學過反射機制看過以上程式碼就可以知道我們可以通過getDeclaredField()返回獲取Un safe類的theUnsafe屬性,然後通過該屬性獲取Unsafe類的例項,因為在Unsafe類裡的theUnsafe屬性已經被new例項化了。

public class Test {
    public static void main(String[] args) throws Exception {
        //通過getDeclaredField方法獲取Unsafe類中名為theUnsafe的屬性
        //注意,該屬性是private型別的,所以不能用getField獲取,只能用getDeclaredField
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        //將該屬性設為可訪問
        field.setAccessible(true);
        //例項該屬性並轉為Unsafe型別
        //因為theUnsafe屬性是Unsafe類所在的包的啟動類載入的,所以可以成功獲得
        Unsafe unsafe = (Unsafe)field.get(null);
    }
}

 

獲取偏移量方法

偏移量

在實際模式中,記憶體是被分成段的,如果想要獲取記憶體中的某個儲存單元,需知道儲存單元的所在段地址(段頭)和偏移量,即使你知道該儲存單元的實際地址。而偏移量就是實際地址與所在段地址(段頭)的距離,偏移量=實際地址-所在段地址(段頭)

舉個例子,假設有個書架,我需要找由左到右、由上到下數的第1024本書,那我只能一本本的數,直到數到第1024本,但如果我知道書架的第4層的第一本書是第1000本書,那我只用從第1000本書開始數,數到1024,只需數1024-1000=24本。在這裡,書架是記憶體,要找的書就是儲存單元,書架的第4層就是記憶體段,第4層的第一本書即書架的第1000本書就是段地址(段頭),第1024本書就是實際地址,而偏移量的就是第1000本書到第1024本書的距離24.

public native long objectFieldOffset(Field var1);

獲取非靜態變數var1的偏移量。

public native long staticFieldOffset(Field var1);

獲取靜態變數var1的偏移量。

public native Object staticFieldBase(Field var1);

獲取靜態變數var1的實際地址,配合staticFieldOffset方法使用,可求出變數所在的段地址

public native int arrayBaseOffset(Class<?> var1);

獲取陣列var1中的第一個元素的偏移量,即陣列的基礎地址。

在記憶體中,陣列的儲存是以一定的偏移量增量連續儲存的,如陣列的第一個元素的實際地址為24,偏移量為4,而陣列的偏移量增量為1,那陣列的第二個元素的實際地址就是25,偏移量為5.

public native int arrayIndexScale(Class<?> var1);

獲取陣列var1的偏移量增量。結合arrayBaseOffset(Class<?> var1)方法就可以求出陣列中各個元素的地址。

 

操作屬性方法

public native Object getObject(Object var1, long var2);

獲取var1物件中偏移量為var2的Object物件,該方法可以無視修飾符限制。相同方法有getInt、getLong、getBoolean等。

public native void putObject(Object var1, long var2, Object var4);

將var1物件中偏移量為var2的Object物件的值設為var4,該方法可以無視修飾符限制。相同的方法有putInt、putLong、putBoolean等。

public native Object getObjectVolatile(Object var1, long var2);

功能與getObject(Object var1, long var2)一樣,但該方法可以保證讀寫的可見性和有序性,可以無視修飾符限制。相同的方法有getIntVolatile、getLongVolatile、getBooleanVolatile等。

public native void putObjectVolatile(Object var1, long var2, Object var4);

功能與putObject(Object var1, long var2, Object var4)一樣,但該方法可以保證讀寫的可見性和有序性,可以無視修飾符限制。相同的方法有putIntVolatile、putLongVolatile、putBooleanVolatile等。

public native void putOrderedObject(Object var1, long var2, Object var4);

功能與putObject(Object var1, long var2, Object var4)一樣,但該方法可以保證讀寫的有序性(不保證可見性),可以無視修飾符限制。相同的方法有putOrderedInt、putOrderedLong等。

 

操作記憶體方法

public native int addressSize();

獲取本地指標大小,單位為byte,通常值為4或8。

public native int pageSize();

獲取本地記憶體的頁數,該返回值會是2的冪次方。

public native long allocateMemory(long var1);

開闢一塊新的記憶體塊,大小為var1(單位為byte),返回新開闢的記憶體塊地址。

public native long reallocateMemory(long var1, long var3);

將記憶體地址為var3的記憶體塊大小調整為var1(單位為byte),返回撥整後新的記憶體塊地址。

public native void setMemory(long var2, long var4, byte var6);

從實際地址var2開始將後面的位元組都修改為var6,修改大小為var4(通常為0)。

public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);

從物件var1中偏移量為var2的地址開始複製,複製到var4中偏移量為var5的地址,複製大小為var7。

當var1為空時,var2就不是偏移量而是實際地址,當var4為空時,var5就不是偏移量而是實際地址。

public native void freeMemory(long var1);

釋放實際地址為var1的記憶體。

 

執行緒掛起和恢復方法

public native void unpark(Object var1);

將被掛起的執行緒var1恢復,由於其不安全性,需保證執行緒var1是存活的。

public native void park(boolean var1, long var2);

當var2等於0時,執行緒會一直掛起,知道呼叫unpark方法才能恢復。

當var2大於0時,如果var1為false,這時var2為增量時間,即執行緒在被掛起var2秒後會自動恢復,如果var1為true,這時var2為絕對時間,即執行緒被掛起後,得到具體的時間var2後才自動恢復。

 

CAS方法

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

CAS機制相關操作,對物件var1裡偏移量為var2的變數進行CAS修改,var4為期待值,var5為修改值,返回修改結果。相同方法有compareAndSwapInt、compareAndSwapLong。

 

類載入方法

public native boolean shouldBeInitialized(Class<?> var1);

判斷var1類是否被初始。

public native void ensureClassInitialized(Class<?> var1);

確保var1類已經被初始化。

public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);

定義一個類,用於動態的建立類。var1為類名,var2為類的檔案資料位元組陣列,var3為var2的輸入起點,var4為輸入長度,var5為載入該類的載入器,var6為保護領域。返回建立後的類。

public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);

用於動態的建立匿名內部類。var1為需建立匿名內部類的類,var2為匿名內部類的檔案資料位元組陣列,var3為修補物件。返回建立後的匿名內部類。

public native Object allocateInstance(Class<?> var1) throws InstantiationException;

建立var1類的例項,但是不會呼叫var1類的構造方法,如果var1類還沒有初始化,則進行初始化。返回建立例項物件。

 

記憶體屏障方法

public native void loadFence();

所有讀操作必須在loadFence方法執行前執行完畢。

public native void storeFence();

所有寫操作必須在storeFence方法執行前執行完畢。

public native void fullFence();

所有讀寫操作必須在fullFence方法執行前執行完畢。

 

疑惑

看到這裡可能有人會有一個疑惑,為什麼這些方法都沒有具體的功能實現程式碼呢?

在文章開頭時就說過,Java不支援直接操作記憶體,那怎麼可能用Java來具體實現功能呢。你可以發現Unsafe類內的大多方法都有native修飾符,native介面可以讓你呼叫本地的程式碼檔案(包括其他語言,如c語言),既然Java實現不了,那就讓能實現的人來做,所以Unsafe的底層實現語言其實是C語言,這也是為什麼Unsafe類內會有偏移量和指標這些Java中沒有的概念了。

 

(以上為本人自己對Unsafe類的理解,如果有錯誤,歡迎各位前輩指出)

 

相關文章