最近為了更加深入瞭解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中沒有的概念了。