淺析 Unsafe 的使用

零 前期準備

0 版本

JDK 版本 : OpenJDK 11.0.1java

IDE : idea 2018.3sql

1 Unsafe 簡介

Unsafe 是 java 留給開發者的後門,用於直接操做系統內存且不受 jvm 管轄,實現相似 C++ 風格的操做。shell

Oracle 官方通常不建議開發者使用 Unsafe 類,由於正如這個類的類名同樣,它並不安全,使用不當會形成內存泄露。api

在平時的業務開發中,這個類基本是不會有接觸到的,可是在 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();

可是其實這個方法是沒法在平常開發中使用的,具體的等下分析。jvm

1 jdk.internal.misc.Unsafe

從代碼量和註釋量上來講,jdk.internal.misc.Unsafe 比另外一者要豐富一些。maven

可是筆者嘗試以後發現該類應該是沒法直接在代碼中使用的。

該類位於 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 併發包的時候常常會接觸到,因此在此先記錄一些看到過的方法的具體應用。其它的有緣補充。

相關文章
相關標籤/搜索