淺析 Unsafe 的使用
零 前期準備
0 版本
JDK 版本 : OpenJDK 11.0.1
IDE : idea 2018.3
1 Unsafe 簡介
Unsafe 是 java 留給開發者的後門,用於直接作業系統記憶體且不受 jvm 管轄,實現類似 C++ 風格的操作。
Oracle 官方一般不建議開發者使用 Unsafe 類,因為正如這個類的類名一樣,它並不安全,使用不當會造成記憶體洩露。
在平時的業務開發中,這個類基本是不會有接觸到的,但是在 java 的併發包和眾多偏向底層的框架中,都有大量應用。
值得一提的是,該類的大部分方法均為 native 修飾,即為直接呼叫的其它語言(大多為 C++)編寫的方法來進行操作,很多細節無法追溯,只能大致瞭解。
一 Unsafe 的獲取
jdk8 中的 Unsafe 在包路徑 sun.misc 下,引用全名 sun.misc.Unsafe。而在 jdk9 中,官方在 jdk.internal.misc 包下又增加了一個 Unsafe 類,引用全名 jdk.internal.misc.Unsafe。
這兩個 Unsafe 的構造方法均被 private 修飾,且類中有一個自身的靜態例項物件,即經典的單例模式實現,並且提供了 getUnsafe() 方法呼叫:
Unsafe unsafe = Unsafe.getUnsafe();
但是其實這個方法是無法在日常開發中使用的,具體的等下分析。
1 jdk.internal.misc.Unsafe
從程式碼量和註釋量上來說,jdk.internal.misc.Unsafe 比另一者要豐富一些。
但是筆者嘗試之後發現該類應該是無法直接在程式碼中使用的。
該類位於 java.base 模組下,根據一些網路資料,筆者嘗試在 idea 的 compiler.xml 檔案中匯入了該模組:
--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED
以及在 maven 的 pom.xml 中加入該模組:
<compilerArgs> <arg>--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED</arg> </compilerArgs>
任然無法使用該類,啟動報錯:
Exception in thread "main" java.lang.IllegalAccessError: class test.jdk.UnsafeTest (in unnamed module @0x57829d67) cannot access class jdk.internal.misc.Unsafe (in module java.base) because module java.base does not export jdk.internal.misc to unnamed module @0x57829d67
有一些大神在博文中提到在 jdk9 中可以使用匯入模組去使用該類,筆者未做嘗試。
可能有一些別的蹊徑可以使用該類,但是筆者對於 jdk 中的模組系統也不算特別熟悉,本題是研究 Unsafe 的使用,所以這部分暫時不多研究了。
2 sun.misc.Unsafe
sun.misc.Unsafe 是 jdk 中一直存在的 Unsafe,一般的第三方庫的實現會使用該類。
該類在 jdk9 之後移動到了 jdk.unsupported 模組中。
在 jdk.unsupported 模組的 module-info.class 中可以看到:
//jdk.unsupported 模組下的 module-info.class module jdk.unsupported { exports com.sun.nio.file; exports sun.misc; //sun.misc.Unsafe 所在的路徑 exports sun.reflect; opens sun.misc; opens sun.reflect; }
也就是說該模組將該類開放了出來,其它應用可以使用該類。
在 jdk11 中,該類的 api 實現很有意思:
//sun.misc.Unsafe.class @ForceInline public void putInt(Object o, long offset, int x) { theInternalUnsafe.putInt(o, offset, x); } @ForceInline public Object getObject(Object o, long offset) { return theInternalUnsafe.getObject(o, offset); } @ForceInline public void putObject(Object o, long offset, Object x) { theInternalUnsafe.putObject(o, offset, x); } @ForceInline public boolean getBoolean(Object o, long offset) { return theInternalUnsafe.getBoolean(o, offset); } @ForceInline public void putBoolean(Object o, long offset, boolean x) { theInternalUnsafe.putBoolean(o, offset, x); } ...
此處僅舉部分例子,在這個 Unsafe 類中,大多數的實現都呼叫了 theInternalUnsafe 這個物件的相關方法。
而這個物件,則是一個 jdk.internal.misc.Unsafe 物件:
//sun.misc.Unsafe.class private static final jdk.internal.misc.Unsafe theInternalUnsafe = jdk.internal.misc.Unsafe.getUnsafe();
在 java.base 的 module-info.class 中筆者也看到了這樣的配置:
//java.base 模組下的 module-info.class exports jdk.internal.misc to //jdk.internal.misc 是 jdk.internal.misc.Unsafe 所在的包路徑 java.desktop, java.logging, java.management, java.naming, java.net.http, java.rmi, java.security.jgss, java.sql, java.xml, jdk.attach, jdk.charsets, jdk.compiler, jdk.internal.vm.ci, jdk.jfr, jdk.jlink, jdk.jshell, jdk.net, jdk.scripting.nashorn, jdk.scripting.nashorn.shell, jdk.unsupported; //jdk.unsupported 是 sun.misc.Unsafe 所在的模組
可見,java.base 只是將該類所在的包路徑開放給了有限的幾個模組,而沒有完全開放給廣大開發者。
看到此處,大致可以猜想,Oracle 應該是希望使用 jdk.internal.misc.Unsafe 作為真正的 Unsafe 使用,但是為了相容性考慮保留了 sun.misc.Unsafe。
並且其實從 api 來說,jdk.internal.misc.Unsafe 的數量更多,許可權更大;sun.misc.Unsafe 則比較有限。
【在這裡說一些題外話,從 jdk.unsupported 這個模組名可以看出,Oracle 確實不太希望開發者使用該模組內的類,甚至 Oracle 在未來的版本里是有可能完全封閉 Unsafe 的使用的,早在 jdk9 時期就有類似傳聞。但是筆者站在一個普通開發者的角度,其實不太希望這樣的情況出現,因為筆者認為 Oracle 作為 java 的標準制定者,應該給 java 留下足夠的自由度,讓開發者能夠充分發揮聰明才智開發出更強大的輪子,成熟的開發者應該能為自己的行為負責,而不需要官方擺出一幅 我來手把手教你 的模樣。】
3 Unsafe 物件獲取
由於 jdk.internal.misc.Unsafe 無法使用,所以以下均使用 sun.misc.Unsafe 來做 demo。
之前提到了 Unsafe 類的 getUnsafe() 靜態獲取單例的方法,但是其實那個方法是不對普通開發者開放的,筆者嘗試使用之後報錯:
Exception in thread "main" java.lang.SecurityException: Unsafe
筆者查看了一些第三方庫對 Unsafe 的使用,也確實不會直接使用該方式,而是使用反射機制去獲取該類:
try { //獲取 Unsafe 內部的私有的例項化單例物件 Field field = Unsafe.class.getDeclaredField("theUnsafe"); //無視許可權 field.setAccessible(true); unsafe = (Unsafe) field.get(null); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }
二 記憶體
在 Unsafe 中可以直接申請一塊記憶體:
//需要傳入一個 long 型別的引數,作為申請的記憶體的大小,單位為 byte //返回這塊記憶體的 long 型別地址 long memoryAddress = unsafe.allocateMemory(8);
Unsafe 申請的記憶體不在 jvm 管轄範圍內,需要手動釋放:
//傳入之前申請的記憶體的地址就可以釋放該塊記憶體了 unsafe.freeMemory(memoryAddress);
注意,如果申請了記憶體,但是中途報錯導致中斷了程式碼執行,沒有執行記憶體的釋放,就出現了記憶體洩漏。所以為了保險起見,實際生產中儘量在 finally 區域裡進行記憶體的釋放操作。
還有一個重新分配記憶體的方法:
//傳入之前申請的記憶體的地址和一個 long 型別的引數作為新的記憶體的 byte 大小 //此方法會釋放掉之前地址的記憶體,然後重新申請一塊符合要求大小的記憶體 //如果之前那塊記憶體上已經存在物件了,就會被拷貝到新的記憶體上 long newMemoryAddress = unsafe.reallocateMemory(memoryAddress, 32);
三 存取物件
Unsafe 中有數量眾多的 put 和 get 方法,用於將物件存入記憶體或者從記憶體中獲取值。原理類似,可以選取幾個來進行理解:
//將 int 型整數 5 存入到指定地址中 unsafe.putInt(menmoryAddress,5); //根據地址獲取到整數 int a = unsafe.getInt(menmoryAddress); //列印,得到 5 System.out.println(a);
這是最基本的 putInt 和 getInt 的運用,除此之外還有 putLong/getLong、putByte/getByte 等等,覆蓋了幾個基本型別。
但是 put 和 get 方法還有一套常用的過載方法,在這裡先借助一個 bean 進行測試:
class UnsafeBean{ //測試1 測試 static 修飾的 int 型別的存取 private static int staticInt = 5; //測試2 測試 static 修飾的 object 型別的存取 private static String staticString = "static_string"; //測試3 測試 final 修飾的 int 型別的存取 private final int finalInt = 5; //測試4 測試 final 修飾的 object 型別的存取 private final String finalString = "final_string"; //測試5 測試一般的 int 型別的存取 private int privateInt; //測試6 測試一般的 object 型別的存取 private String privateString; }
測試內容:
UnsafeBean bean = new UnsafeBean(); //1 測試 staticInt //先通過變數名反射獲取到該變數 Field staticIntField = UnsafeBean.class.getDeclaredField("staticInt"); //無視許可權 staticIntField.setAccessible(true); //staticFieldOffset(...) 方法能夠獲取到類中的 static 修飾的變數 long staticIntAddress = unsafe.staticFieldOffset(staticIntField); //使用 put 方法進行值改變,需要傳入其所在的 class 物件、記憶體地址和新的值 unsafe.putInt(UnsafeBean.class,staticIntAddress,10); //使用 get 方法去獲取值,需要傳入其所在的 class 物件和記憶體地址 int stiatcIntTest = unsafe.getInt(UnsafeBean.class,staticIntAddress); //此處輸出為 10 System.out.println(stiatcIntTest); //2 測試 staticString //基本流程相同,只是 put 和 get 方法換成了 getObject(...) 和 putObject(...) Field staticStringField = UnsafeBean.class.getDeclaredField("staticString"); staticStringField.setAccessible(true); long staticStringAddress = unsafe.staticFieldOffset(staticStringField); unsafe.putObject(UnsafeBean.class,staticStringAddress,"static_string_2"); String staticStringTest = (String)unsafe.getObject(UnsafeBean.class,staticStringAddress); ///此處輸出為 static_string_2 System.out.println(staticStringTest); //3 測試 finalInt //基本流程相同,只是 staticFieldOffset(...) 方法換成了 objectFieldOffset(...) 方法 Field finalIntField = UnsafeBean.class.getDeclaredField("finalInt"); finalIntField.setAccessible(true); long finalIntAddress = unsafe.objectFieldOffset(finalIntField); //需要注意的是,雖然該變數是 final 修飾的,理論上是不可變的變數,但是 unsafe 是具有修改許可權的 unsafe.putInt(bean,finalIntAddress,10); int finalIntTest = unsafe.getInt(bean,finalIntAddress); //此處輸出為 10 System.out.println(finalIntTest); //4 測試 finalString Field finalStringField = UnsafeBean.class.getDeclaredField("finalString"); finalStringField.setAccessible(true); long finalStringAddress = unsafe.objectFieldOffset(finalStringField); unsafe.putInt(bean,finalStringAddress,"final_string_2"); String finalStringTest = (String)unsafe.getObject(bean,finalStringAddress); ///此處輸出為 final_string_2 System.out.println(finalStringTest); //測試5 和 測試6 此處省略,因為和上述 final 部分的測試程式碼一模一樣
put 和 get 方法還有一組很類似的 api,是帶 volatile 的:
public int getIntVolatile(Object o, long offset); public void putIntVolatile(Object o, long offset, int x); public Object getObjectVolatile(Object o, long offset); public void putObjectVolatile(Object o, long offset, Object x); ...
這一組 api 的使用方式和上述一樣,只是增加了對 volatile 關鍵詞的支援。測試發現,該組 api 也支援不使用 volatile 關鍵詞的變數。
get 和 put 方法的思路都比較簡單,使用思路可以歸納為:
1 用反射獲取變數物件 (getDeclaredField) 2 開放許可權,遮蔽 private 關鍵字的影響 (setAccessible(true)) 3 呼叫相關方法獲取到該物件中的該變數物件的記憶體地址 (staticFieldOffset/objectFieldOffset) 4 通過記憶體地址去修改該物件的值 (putInt/putObject) 5 獲取物件的值 (getInt/getObject)
四 執行緒的掛起和恢復
執行緒的掛起呼叫 park(...) 方法:
//該方法第二個引數為 long 型別物件,表示該執行緒準備掛起到的時間點 //注意,此為時間點,而非時間,該時間點從 1970 年(即元年)開始 //第一個引數為 boolean 型別的物件,用來表示掛起時間的單位,true 表示毫秒,false 表示納秒 //第一個引數為 true,第二個引數為 0 的時候,執行緒會直接返回,不太清楚機理 unsafe.park(false,0L);
與之對應的 unpark(...) 方法:
//此處傳入執行緒物件 unsafe.unpark(thread);
請注意,掛起時是不需要傳入執行緒物件的,即只有執行緒自身可以執行此方法用於掛起自身,但是恢復方法是需要其它執行緒來幫助恢復的。
五 CAS
Unsafe 中提供了一套原子化的判斷和值替換 api,來看一下例子:
//建立一個 Integer 物件,value 為 1 Integer i = 1; //獲取到內部變數 value,這個變數用於存放值 Field valueField = Integer.class.getDeclaredField("value"); valueField.setAccessible(true); //獲取到記憶體地址 long valueAddress = unsafe.objectFieldOffset(valueField); //該方法使用者比較及替換值 //第一個引數為要替換的物件本身,第二個引數為值的記憶體地址 //第三個引數為變數的預期值,第四個引數為變數要換的值 //如果變數目前的值等於預期值(第三個引數),則會將變數的值換成新值(第四個引數),返回 true //如果不等於預期,則不會改變,並返回 false boolean isOk = unsafe.compareAndSwapInt(i,valueAddress,1,5); //此處輸出 true System.out.println(isOk); //此處輸出 5 System.out.println(i);
六 一點嘮叨
Unsafe 的 api 眾多,但是網路資料不多,且功能較為晦澀,不太好寫 demo。但是在近期學習 jdk 併發包的時候經常會接觸到,所以在此先記錄一些看到過的方法的具體應用。其它的有緣補充。