一個名爲不安全的類Unsafe

最近爲了更加深刻了解NIO的實現原理,學習NIO的源碼時,遇到了一個問題。即在WindowsSelectorImpl中的java

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

 

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的反射機制來獲取。app

@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)。jvm

  • @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類的理解,若是有錯誤,歡迎各位前輩指出)

相關文章
相關標籤/搜索