最近爲了更加深刻了解NIO的實現原理,學習NIO的源碼時,遇到了一個問題。即在WindowsSelectorImpl中的java
pollWrapper屬性,當我點進去查看它的PollArrayWrapper類型時,發現它和AllocatedNativeObject類型有關,而AllocatedNativeObject繼承了NativeObject類,隨着又發現了NativeObject是基於一個Unsafe類實現的。不安全的類????api
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後才自動恢復。
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中沒有的概念了。