JAVA中神奇的雙刃劍--Unsafe

 

參考資料:

Unsafe介紹

在Oracle的Jdk8無法獲取到sun.misc包的源碼,想看此包的源碼可以直接下載openjdk,包的路徑是:

  • openjdk-8u40-src-b25-10_feb_2015\openjdk\jdk\src\share\classes\sun\misc。

當然,不同的openjdk版本的根目錄(這裏是openjdk-8u40-src-b25-10_feb_2015)不一定相同。sun.misc包含了低級(native硬件級別的原子操作)、不安全的操作集合。

Java無法直接訪問到操作系統底層(如系統硬件等),爲此Java使用native方法來擴展Java程序的功能。Unsafe類提供了硬件級別的原子操作,提供了一些繞開JVM的更底層功能,由此提高效率。本文的Unsafe類來源於openjdk-8u40-src-b25-10_feb_2015。

Unsafe的使用建議#

建議先看這個知乎帖子第一樓R大的回答:爲什麼JUC中大量使用了sun.misc.Unsafe 這個類,但官方卻不建議開發者使用

使用Unsafe要注意以下幾個問題:

  • 1、Unsafe有可能在未來的Jdk版本移除或者不允許Java應用代碼使用,這一點可能導致使用了Unsafe的應用無法運行在高版本的Jdk。
  • 2、Unsafe的不少方法中必須提供原始地址(內存地址)和被替換對象的地址,偏移量要自己計算,一旦出現問題就是JVM崩潰級別的異常,會導致整個JVM實例崩潰,表現爲應用程序直接crash掉。
  • 3、Unsafe提供的直接內存訪問的方法中使用的內存不受JVM管理(無法被GC),需要手動管理,一旦出現疏忽很有可能成爲內存泄漏的源頭。

暫時總結出以上三點問題。Unsafe在JUC(java.util.concurrent)包中大量使用(主要是CAS),在netty中方便使用直接內存,還有一些高併發的交易系統爲了提高CAS的效率也有可能直接使用到Unsafe。總而言之,Unsafe類是一把雙刃劍。

Unsafe詳解

Unsafe中一共有82個public native修飾的方法,還有幾十個基於這82個public native方法的其他方法。


 1     //擴充內存  
 2     public native long reallocateMemory(long address, long bytes);  
 3       
 4     //分配內存  
 5     public native long allocateMemory(long bytes);  
 6       
 7     //釋放內存  
 8     public native void freeMemory(long address);  
 9       
10     //在給定的內存塊中設置值  
11     public native void setMemory(Object o, long offset, long bytes, byte value);  
12       
13     //從一個內存塊拷貝到另一個內存塊  
14     public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);  
15       
16     //獲取值,不管java的訪問限制,其他有類似的getInt,getDouble,getLong,getChar等等  
17     public native Object getObject(Object o, long offset);  
18       
19     //設置值,不管java的訪問限制,其他有類似的putInt,putDouble,putLong,putChar等等  
20     public native void putObject(Object o, long offset);  
21       
22     //從一個給定的內存地址獲取本地指針,如果不是allocateMemory方法的,結果將不確定  
23     public native long getAddress(long address);  
24       
25     //存儲一個本地指針到一個給定的內存地址,如果地址不是allocateMemory方法的,結果將不確定  
26     public native void putAddress(long address, long x);  
27       
28     //該方法返回給定field的內存地址偏移量,這個值對於給定的filed是唯一的且是固定不變的  
29     public native long staticFieldOffset(Field f);  
30       
31     //報告一個給定的字段的位置,不管這個字段是private,public還是保護類型,和staticFieldBase結合使用  
32     public native long objectFieldOffset(Field f);  
33       
34     //獲取一個給定字段的位置  
35     public native Object staticFieldBase(Field f);  
36       
37     //確保給定class被初始化,這往往需要結合基類的靜態域(field)  
38     public native void ensureClassInitialized(Class c);  
39       
40     //可以獲取數組第一個元素的偏移地址  
41     public native int arrayBaseOffset(Class arrayClass);  
42       
43     //可以獲取數組的轉換因子,也就是數組中元素的增量地址。將arrayBaseOffset與arrayIndexScale配合使用, 可以定位數組中每個元素在內存中的位置  
44     public native int arrayIndexScale(Class arrayClass);  
45       
46     //獲取本機內存的頁數,這個值永遠都是2的冪次方  
47     public native int pageSize();  
48       
49     //告訴虛擬機定義了一個沒有安全檢查的類,默認情況下這個類加載器和保護域來着調用者類  
50     public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);  
51       
52     //定義一個類,但是不讓它知道類加載器和系統字典  
53     public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);  
54       
55     //鎖定對象,必須是沒有被鎖的
56     public native void monitorEnter(Object o);  
57       
58     //解鎖對象  
59     public native void monitorExit(Object o);  
60       
61     //試圖鎖定對象,返回true或false是否鎖定成功,如果鎖定,必須用monitorExit解鎖  
62     public native boolean tryMonitorEnter(Object o);  
63       
64     //引發異常,沒有通知  
65     public native void throwException(Throwable ee);  
66       
67     //CAS,如果對象偏移量上的值=期待值,更新爲x,返回true.否則false.類似的有compareAndSwapInt,compareAndSwapLong,compareAndSwapBoolean,compareAndSwapChar等等。  
68     public final native boolean compareAndSwapObject(Object o, long offset,  Object expected, Object x);  
69       
70     // 該方法獲取對象中offset偏移地址對應的整型field的值,支持volatile load語義。類似的方法有getIntVolatile,getBooleanVolatile等等  
71     public native Object getObjectVolatile(Object o, long offset);   
72       
73     //線程調用該方法,線程將一直阻塞直到超時,或者是中斷條件出現。  
74     public native void park(boolean isAbsolute, long time);  
75       
76     //終止掛起的線程,恢復正常.java.util.concurrent包中掛起操作都是在LockSupport類實現的,也正是使用這兩個方法
77     public native void unpark(Object thread);  
78       
79     //獲取系統在不同時間系統的負載情況  
80     public native int getLoadAverage(double[] loadavg, int nelems);  
81       
82     //創建一個類的實例,不需要調用它的構造函數、初使化代碼、各種JVM安全檢查以及其它的一些底層的東西。即使構造函數是私有,我們也可以通過這個方法創建它的實例,對於單例模式,簡直是噩夢,哈哈  
83     public native Object allocateInstance(Class cls) throws InstantiationException;  

類、對象和變量相關方法#

主要包括類的非常規實例化、基於偏移地址獲取或者設置變量的值、基於偏移地址獲取或者設置數組元素的值等。

getObject#

  • public native Object getObject(Object o, long offset);

通過給定的Java變量獲取引用值。這裏實際上是獲取一個Java對象o中,獲取偏移地址爲offset的屬性的值,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。類似的方法有getInt、getDouble等等。

putObject#

  • public native void putObject(Object o, long offset, Object x);

將引用值存儲到給定的Java變量中。這裏實際上是設置一個Java對象o中偏移地址爲offset的屬性的值爲x,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。類似的方法有putInt、putDouble等等。

getObjectVolatile#

  • public native Object getObjectVolatile(Object o, long offset);

此方法和上面的getObject功能類似,不過附加了'volatile'加載語義,也就是強制從主存中獲取屬性值。類似的方法有getIntVolatile、getDoubleVolatile等等。這個方法要求被使用的屬性被volatile修飾,否則功能和getObject方法相同。

putObjectVolatile#

  • public native void putObjectVolatile(Object o, long offset, Object x);

此方法和上面的putObject功能類似,不過附加了'volatile'加載語義,也就是設置值的時候強制(JMM會保證獲得鎖到釋放鎖之間所有對象的狀態更新都會在鎖被釋放之後)更新到主存,從而保證這些變更對其他線程是可見的。類似的方法有putIntVolatile、putDoubleVolatile等等。這個方法要求被使用的屬性被volatile修飾,否則功能和putObject方法相同。

putOrderedObject#

  • public native void putOrderedObject(Object o, long offset, Object x);

設置o對象中offset偏移地址offset對應的Object型field的值爲指定值x。這是一個有序或者有延遲的putObjectVolatile方法,並且不保證值的改變被其他線程立即看到。只有在field被volatile修飾並且期望被修改的時候使用纔會生效。類似的方法有putOrderedIntputOrderedLong

staticFieldOffset#

  • public native long staticFieldOffset(Field f);

返回給定的靜態屬性在它的類的存儲分配中的位置(偏移地址)。不要在這個偏移量上執行任何類型的算術運算,它只是一個被傳遞給不安全的堆內存訪問器的cookie。注意:這個方法僅僅針對靜態屬性,使用在非靜態屬性上會拋異常。下面源碼中的方法註釋估計有誤,staticFieldOffset和objectFieldOffset的註釋估計是對調了,爲什麼會出現這個問題無法考究。

objectFieldOffset#

  • public native long objectFieldOffset(Field f);

返回給定的非靜態屬性在它的類的存儲分配中的位置(偏移地址)。不要在這個偏移量上執行任何類型的算術運算,它只是一個被傳遞給不安全的堆內存訪問器的cookie。注意:這個方法僅僅針對非靜態屬性,使用在靜態屬性上會拋異常。

staticFieldBase#

  • public native Object staticFieldBase(Field f);

返回給定的靜態屬性的位置,配合staticFieldOffset方法使用。實際上,這個方法返回值就是靜態屬性所在的Class對象的一個內存快照。註釋中說到,此方法返回的Object有可能爲null,它只是一個'cookie'而不是真實的對象,不要直接使用的它的實例中的獲取屬性和設置屬性的方法,它的作用只是方便調用上面提到的像getInt(Object,long)等等的任意方法。

shouldBeInitialized#

  • public native boolean shouldBeInitialized(Class<?> c);

檢測給定的類是否需要初始化。通常需要使用在獲取一個類的靜態屬性的時候(因爲一個類如果沒初始化,它的靜態屬性也不會初始化)。 此方法當且僅當ensureClassInitialized方法不生效的時候才返回false。

ensureClassInitialized#

  • public native void ensureClassInitialized(Class<?> c);

檢測給定的類是否已經初始化。通常需要使用在獲取一個類的靜態屬性的時候(因爲一個類如果沒初始化,它的靜態屬性也不會初始化)。

arrayBaseOffset#

  • public native int arrayBaseOffset(Class<?> arrayClass);

返回數組類型的第一個元素的偏移地址(基礎偏移地址)。如果arrayIndexScale方法返回的比例因子不爲0,你可以通過結合基礎偏移地址和比例因子訪問數組的所有元素。Unsafe中已經初始化了很多類似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。

arrayIndexScale#

  • public native int arrayIndexScale(Class<?> arrayClass);

返回數組類型的比例因子(其實就是數據中元素偏移地址的增量,因爲數組中的元素的地址是連續的)。此方法不適用於數組類型爲"narrow"類型的數組,"narrow"類型的數組類型使用此方法會返回0(這裏narrow應該是狹義的意思,但是具體指哪些類型暫時不明確,筆者查了很多資料也沒找到結果)。Unsafe中已經初始化了很多類似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。

defineClass#

  • public native Class<?> defineClass(String name, byte[] b, int off, int len,ClassLoader loader,ProtectionDomain protectionDomain);

告訴JVM定義一個類,返回類實例,此方法會跳過JVM的所有安全檢查。默認情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例應該來源於調用者。

defineAnonymousClass#

  • public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

這個方法的使用可以看R大的知乎回答:JVM crashes at libjvm.so,下面截取一點內容解釋此方法。

  • 1、VM Anonymous Class可以看作一種模板機制,如果程序要動態生成很多結構相同、只是若干變量不同的類的話,可以先創建出一個包含佔位符常量的正常類作爲模板,然後利用sun.misc.Unsafe#defineAnonymousClass()方法,傳入該類(host class,宿主類或者模板類)以及一個作爲"constant pool path"的數組來替換指定的常量爲任意值,結果得到的就是一個替換了常量的VM Anonymous Class。
  • 2、VM Anonymous Class從VM的角度看是真正的"沒有名字"的,在構造出來之後只能通過Unsafe#defineAnonymousClass()返回出來一個Class實例來進行反射操作。

還有其他幾點看以自行閱讀。這個方法雖然翻譯爲"定義匿名類",但是它所定義的類和實際的匿名類有點不相同,因此一般情況下我們不會用到此方法。在Jdk中lambda表達式相關的東西用到它,可以看InnerClassLambdaMetafactory這個類。

allocateInstance#

  • public native Object allocateInstance(Class<?> cls) throws InstantiationException;

通過Class對象創建一個類的實例,不需要調用其構造函數、初始化代碼、JVM安全檢查等等。同時,它抑制修飾符檢測,也就是即使構造器是private修飾的也能通過此方法實例化。

內存管理#

addressSize#

  • public native int addressSize();

獲取本地指針的大小(單位是byte),通常值爲4或者8。常量ADDRESS_SIZE就是調用此方法。

pageSize#

  • public native int pageSize();

獲取本地內存的頁數,此值爲2的冪次方。

allocateMemory#

  • public native long allocateMemory(long bytes);

分配一塊新的本地內存,通過bytes指定內存塊的大小(單位是byte),返回新開闢的內存的地址。如果內存塊的內容不被初始化,那麼它們一般會變成內存垃圾。生成的本機指針永遠不會爲零,並將對所有值類型進行對齊。可以通過freeMemory方法釋放內存塊,或者通過reallocateMemory方法調整內存塊大小。bytes值爲負數或者過大會拋出IllegalArgumentException異常,如果系統拒絕分配內存會拋出OutOfMemoryError異常。

reallocateMemory#

  • public native long reallocateMemory(long address, long bytes);

通過指定的內存地址address重新調整本地內存塊的大小,調整後的內存塊大小通過bytes指定(單位爲byte)。可以通過freeMemory方法釋放內存塊,或者通過reallocateMemory方法調整內存塊大小。bytes值爲負數或者過大會拋出IllegalArgumentException異常,如果系統拒絕分配內存會拋出OutOfMemoryError異常。

setMemory#

  • public native void setMemory(Object o, long offset, long bytes, byte value);

將給定內存塊中的所有字節設置爲固定值(通常是0)。內存塊的地址由對象引用o和偏移地址共同決定,如果對象引用o爲null,offset就是絕對地址。第三個參數就是內存塊的大小,如果使用allocateMemory進行內存開闢的話,這裏的值應該和allocateMemory的參數一致。value就是設置的固定值,一般爲0(這裏可以參考netty的DirectByteBuffer)。一般而言,o爲null,所有有個重載方法是public native void setMemory(long offset, long bytes, byte value);,等效於setMemory(null, long offset, long bytes, byte value);

多線程同步#

主要包括監視器鎖定、解鎖以及CAS相關的方法。

monitorEnter#

  • public native void monitorEnter(Object o);

鎖定對象,必須通過monitorExit方法才能解鎖。此方法經過實驗是可以重入的,也就是可以多次調用,然後通過多次調用monitorExit進行解鎖。

monitorExit#

  • public native void monitorExit(Object o);

解鎖對象,前提是對象必須已經調用monitorEnter進行加鎖,否則拋出IllegalMonitorStateException異常。

tryMonitorEnter#

  • public native boolean tryMonitorEnter(Object o);

嘗試鎖定對象,如果加鎖成功返回true,否則返回false。必須通過monitorExit方法才能解鎖。

compareAndSwapObject#

  • public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);

針對Object對象進行CAS操作。即是對應Java變量引用o,原子性地更新o中偏移地址爲offset的屬性的值爲x,當且僅的偏移地址爲offset的屬性的當前值爲expected纔會更新成功返回true,否則返回false。

  • o:目標Java變量引用。
  • offset:目標Java變量中的目標屬性的偏移地址。
  • expected:目標Java變量中的目標屬性的期望的當前值。
  • x:目標Java變量中的目標屬性的目標更新值。

類似的方法有compareAndSwapIntcompareAndSwapLong,在Jdk8中基於CAS擴展出來的方法有getAndAddIntgetAndAddLonggetAndSetIntgetAndSetLonggetAndSetObject,它們的作用都是:通過CAS設置新的值,返回舊的值。

線程的掛起和恢復#

unpark#

  • public native void unpark(Object thread);

釋放被park創建的在一個線程上的阻塞。這個方法也可以被使用來終止一個先前調用park導致的阻塞。這個操作是不安全的,因此必須保證線程是存活的(thread has not been destroyed)。從Java代碼中判斷一個線程是否存活的是顯而易見的,但是從native代碼中這機會是不可能自動完成的。

park#

  • public native void park(boolean isAbsolute, long time);

阻塞當前線程直到一個unpark方法出現(被調用)、一個用於unpark方法已經出現過(在此park方法調用之前已經調用過)、線程被中斷或者time時間到期(也就是阻塞超時)。在time非零的情況下,如果isAbsolute爲true,time是相對於新紀元之後的毫秒,否則time表示納秒。這個方法執行時也可能不合理地返回(沒有具體原因)。併發包java.util.concurrent中的框架對線程的掛起操作被封裝在LockSupport類中,LockSupport類中有各種版本pack方法,但最終都調用了Unsafe#park()方法。

內存屏障#

內存屏障相關的方法是在Jdk8添加的。內存屏障相關的知識可以先自行查閱。

loadFence#

  • public native void loadFence();

在該方法之前的所有讀操作,一定在load屏障之前執行完成。

storeFence#

  • public native void storeFence();

在該方法之前的所有寫操作,一定在store屏障之前執行完成

fullFence#

  • public native void fullFence();

在該方法之前的所有讀寫操作,一定在full屏障之前執行完成,這個內存屏障相當於上面兩個(load屏障和store屏障)的合體功能。

其他#

getLoadAverage#

  • public native int getLoadAverage(double[] loadavg, int nelems);

獲取系統的平均負載值,loadavg這個double數組將會存放負載值的結果,nelems決定樣本數量,nelems只能取值爲1到3,分別代表最近1、5、15分鐘內系統的平均負載。如果無法獲取系統的負載,此方法返回-1,否則返回獲取到的樣本數量(loadavg中有效的元素個數)。實驗中這個方法一直返回-1,其實完全可以使用JMX中的相關方法替代此方法。

throwException#

  • public native void throwException(Throwable ee);

繞過檢測機制直接拋出異常。

作者: throwable 、只會一點java 

出處:https://www.cnblogs.com/throwable/p/9139947.html、 https://www.cnblogs.com/dennyzhangdd/p/7230012.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章