參考資料:
Java魔法類:sun.misc.Unsafe
在openjdk8下看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。
建議先看這個知乎文章第一樓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中一共有82個public native修飾的方法,還有幾十個基于這82個public native方法的其他方法。
初始化的代碼主要包括調用JVM本地方法<code>registerNatives()</code>和<code>sun.reflect.Reflection#registerMethodsToFilter</code>。然後建立一個Unsafe執行個體命名為theUnsafe,通過靜态方法<code>getUnsafe()</code>擷取,擷取的時候需要做權限判斷。由此可見,Unsafe使用了單例設計(可見構造私有化了)。Unsafe類做了限制,如果是普通的調用的話,它會抛出一個SecurityException異常;隻有由主類加載器(BootStrap classLoader)加載的類才能調用這個類中的方法。最簡單的使用方式是基于反射擷取Unsafe執行個體。
主要包括類的非正常執行個體化、基于偏移位址擷取或者設定變量的值、基于偏移位址擷取或者設定數組元素的值等。
<code>public native Object getObject(Object o, long offset);</code>
通過給定的Java變量擷取引用值。這裡實際上是擷取一個Java對象o中,擷取偏移位址為offset的屬性的值,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。類似的方法有getInt、getDouble等等。
<code>public native void putObject(Object o, long offset, Object x);</code>
将引用值存儲到給定的Java變量中。這裡實際上是設定一個Java對象o中偏移位址為offset的屬性的值為x,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。類似的方法有putInt、putDouble等等。
<code>public native Object getObjectVolatile(Object o, long offset);</code>
此方法和上面的<code>getObject</code>功能類似,不過附加了'volatile'加載語義,也就是強制從主存中擷取屬性值。類似的方法有getIntVolatile、getDoubleVolatile等等。這個方法要求被使用的屬性被volatile修飾,否則功能和<code>getObject</code>方法相同。
<code>public native void putObjectVolatile(Object o, long offset, Object x);</code>
此方法和上面的<code>putObject</code>功能類似,不過附加了'volatile'加載語義,也就是設定值的時候強制(JMM會保證獲得鎖到釋放鎖之間所有對象的狀态更新都會在鎖被釋放之後)更新到主存,進而保證這些變更對其他線程是可見的。類似的方法有putIntVolatile、putDoubleVolatile等等。這個方法要求被使用的屬性被volatile修飾,否則功能和<code>putObject</code>方法相同。
<code>public native void putOrderedObject(Object o, long offset, Object x);</code>
設定o對象中offset偏移位址offset對應的Object型field的值為指定值x。這是一個有序或者有延遲的putObjectVolatile方法,并且不保證值的改變被其他線程立即看到。隻有在field被volatile修飾并且期望被修改的時候使用才會生效。類似的方法有<code>putOrderedInt</code>和<code>putOrderedLong</code>。
<code>public native long staticFieldOffset(Field f);</code>
傳回給定的靜态屬性在它的類的存儲配置設定中的位置(偏移位址)。不要在這個偏移量上執行任何類型的算術運算,它隻是一個被傳遞給不安全的堆記憶體通路器的cookie。注意:這個方法僅僅針對靜态屬性,使用在非靜态屬性上會抛異常。下面源碼中的方法注釋估計有誤,staticFieldOffset和objectFieldOffset的注釋估計是對調了,為什麼會出現這個問題無法考究。
<code>public native long objectFieldOffset(Field f);</code>
傳回給定的非靜态屬性在它的類的存儲配置設定中的位置(偏移位址)。不要在這個偏移量上執行任何類型的算術運算,它隻是一個被傳遞給不安全的堆記憶體通路器的cookie。注意:這個方法僅僅針對非靜态屬性,使用在靜态屬性上會抛異常。
<code>public native Object staticFieldBase(Field f);</code>
傳回給定的靜态屬性的位置,配合staticFieldOffset方法使用。實際上,這個方法傳回值就是靜态屬性所在的Class對象的一個記憶體快照。注釋中說到,此方法傳回的Object有可能為null,它隻是一個'cookie'而不是真實的對象,不要直接使用的它的執行個體中的擷取屬性和設定屬性的方法,它的作用隻是友善調用上面提到的像<code>getInt(Object,long)</code>等等的任意方法。
<code>public native boolean shouldBeInitialized(Class<?> c);</code>
檢測給定的類是否需要初始化。通常需要使用在擷取一個類的靜态屬性的時候(因為一個類如果沒初始化,它的靜态屬性也不會初始化)。 此方法當且僅當<code>ensureClassInitialized</code>方法不生效的時候才傳回false。
<code>public native void ensureClassInitialized(Class<?> c);</code>
檢測給定的類是否已經初始化。通常需要使用在擷取一個類的靜态屬性的時候(因為一個類如果沒初始化,它的靜态屬性也不會初始化)。
<code>public native int arrayBaseOffset(Class<?> arrayClass);</code>
傳回數組類型的第一個元素的偏移位址(基礎偏移位址)。如果<code>arrayIndexScale</code>方法傳回的比例因子不為0,你可以通過結合基礎偏移位址和比例因子通路數組的所有元素。Unsafe中已經初始化了很多類似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。
<code>public native int arrayIndexScale(Class<?> arrayClass);</code>
傳回數組類型的比例因子(其實就是資料中元素偏移位址的增量,因為數組中的元素的位址是連續的)。此方法不适用于數組類型為"narrow"類型的數組,"narrow"類型的數組類型使用此方法會傳回0(這裡narrow應該是狹義的意思,但是具體指哪些類型暫時不明确,筆者查了很多資料也沒找到結果)。Unsafe中已經初始化了很多類似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。
<code>public native Class<?> defineClass(String name, byte[] b, int off, int len,ClassLoader loader,ProtectionDomain protectionDomain);</code>
告訴JVM定義一個類,傳回類執行個體,此方法會跳過JVM的所有安全檢查。預設情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)執行個體應該來源于調用者。
<code>public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);</code>
這個方法的使用可以看R大的知乎回答:JVM crashes at libjvm.so,下面截取一點内容解釋此方法。
1、VM Anonymous Class可以看作一種模闆機制,如果程式要動态生成很多結構相同、隻是若幹變量不同的類的話,可以先建立出一個包含占位符常量的正常類作為模闆,然後利用<code>sun.misc.Unsafe#defineAnonymousClass()</code>方法,傳入該類(host class,宿主類或者模闆類)以及一個作為"constant pool path"的數組來替換指定的常量為任意值,結果得到的就是一個替換了常量的VM Anonymous Class。
2、VM Anonymous Class從VM的角度看是真正的"沒有名字"的,在構造出來之後隻能通過<code>Unsafe#defineAnonymousClass()</code>傳回出來一個Class執行個體來進行反射操作。
還有其他幾點看以自行閱讀。這個方法雖然翻譯為"定義匿名類",但是它所定義的類和實際的匿名類有點不相同,是以一般情況下我們不會用到此方法。在Jdk中lambda表達式相關的東西用到它,可以看InnerClassLambdaMetafactory這個類。
<code>public native Object allocateInstance(Class<?> cls) throws InstantiationException;</code>
通過Class對象建立一個類的執行個體,不需要調用其構造函數、初始化代碼、JVM安全檢查等等。同時,它抑制修飾符檢測,也就是即使構造器是private修飾的也能通過此方法執行個體化。
<code>public native int addressSize();</code>
擷取本地指針的大小(機關是byte),通常值為4或者8。常量ADDRESS_SIZE就是調用此方法。
<code>public native int pageSize();</code>
擷取本地記憶體的頁數,此值為2的幂次方。
<code>public native long allocateMemory(long bytes);</code>
配置設定一塊新的本地記憶體,通過bytes指定記憶體塊的大小(機關是byte),傳回新開辟的記憶體的位址。如果記憶體塊的内容不被初始化,那麼它們一般會變成記憶體垃圾。生成的本機指針永遠不會為零,并将對所有值類型進行對齊。可以通過<code>freeMemory</code>方法釋放記憶體塊,或者通過<code>reallocateMemory</code>方法調整記憶體塊大小。bytes值為負數或者過大會抛出IllegalArgumentException異常,如果系統拒絕配置設定記憶體會抛出OutOfMemoryError異常。
<code>public native long reallocateMemory(long address, long bytes);</code>
通過指定的記憶體位址address重新調整本地記憶體塊的大小,調整後的記憶體塊大小通過bytes指定(機關為byte)。可以通過<code>freeMemory</code>方法釋放記憶體塊,或者通過<code>reallocateMemory</code>方法調整記憶體塊大小。bytes值為負數或者過大會抛出IllegalArgumentException異常,如果系統拒絕配置設定記憶體會抛出OutOfMemoryError異常。
<code>public native void setMemory(Object o, long offset, long bytes, byte value);</code>
将給定記憶體塊中的所有位元組設定為固定值(通常是0)。記憶體塊的位址由對象引用o和偏移位址共同決定,如果對象引用o為null,offset就是絕對位址。第三個參數就是記憶體塊的大小,如果使用<code>allocateMemory</code>進行記憶體開辟的話,這裡的值應該和<code>allocateMemory</code>的參數一緻。value就是設定的固定值,一般為0(這裡可以參考netty的DirectByteBuffer)。一般而言,o為null,所有有個重載方法是<code>public native void setMemory(long offset, long bytes, byte value);</code>,等效于<code>setMemory(null, long offset, long bytes, byte value);</code>。
主要包括螢幕鎖定、解鎖以及CAS相關的方法。
<code>public native void monitorEnter(Object o);</code>
鎖定對象,必須通過<code>monitorExit</code>方法才能解鎖。此方法經過實驗是可以重入的,也就是可以多次調用,然後通過多次調用<code>monitorExit</code>進行解鎖。
<code>public native void monitorExit(Object o);</code>
解鎖對象,前提是對象必須已經調用<code>monitorEnter</code>進行加鎖,否則抛出IllegalMonitorStateException異常。
<code>public native boolean tryMonitorEnter(Object o);</code>
嘗試鎖定對象,如果加鎖成功傳回true,否則傳回false。必須通過<code>monitorExit</code>方法才能解鎖。
<code>public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);</code>
針對Object對象進行CAS操作。即是對應Java變量引用o,原子性地更新o中偏移位址為offset的屬性的值為x,當且僅的偏移位址為offset的屬性的目前值為expected才會更新成功傳回true,否則傳回false。
o:目标Java變量引用。
offset:目标Java變量中的目标屬性的偏移位址。
expected:目标Java變量中的目标屬性的期望的目前值。
x:目标Java變量中的目标屬性的目标更新值。
類似的方法有<code>compareAndSwapInt</code>和<code>compareAndSwapLong</code>,在Jdk8中基于CAS擴充出來的方法有<code>getAndAddInt</code>、<code>getAndAddLong</code>、<code>getAndSetInt</code>、<code>getAndSetLong</code>、<code>getAndSetObject</code>,它們的作用都是:通過CAS設定新的值,傳回舊的值。
<code>public native void unpark(Object thread);</code>
釋放被<code>park</code>建立的在一個線程上的阻塞。這個方法也可以被使用來終止一個先前調用<code>park</code>導緻的阻塞。這個操作是不安全的,是以必須保證線程是存活的(thread has not been destroyed)。從Java代碼中判斷一個線程是否存活的是顯而易見的,但是從native代碼中這機會是不可能自動完成的。
<code>public native void park(boolean isAbsolute, long time);</code>
阻塞目前線程直到一個<code>unpark</code>方法出現(被調用)、一個用于<code>unpark</code>方法已經出現過(在此park方法調用之前已經調用過)、線程被中斷或者time時間到期(也就是阻塞逾時)。在time非零的情況下,如果isAbsolute為true,time是相對于新紀元之後的毫秒,否則time表示納秒。這個方法執行時也可能不合理地傳回(沒有具體原因)。并發包java.util.concurrent中的架構對線程的挂起操作被封裝在LockSupport類中,LockSupport類中有各種版本pack方法,但最終都調用了<code>Unsafe#park()</code>方法。
記憶體屏障相關的方法是在Jdk8添加的。記憶體屏障相關的知識可以先自行查閱。
<code>public native void loadFence();</code>
在該方法之前的所有讀操作,一定在load屏障之前執行完成。
<code>public native void storeFence();</code>
在該方法之前的所有寫操作,一定在store屏障之前執行完成
<code>public native void fullFence();</code>
在該方法之前的所有讀寫操作,一定在full屏障之前執行完成,這個記憶體屏障相當于上面兩個(load屏障和store屏障)的合體功能。
<code>public native int getLoadAverage(double[] loadavg, int nelems);</code>
擷取系統的平均負載值,loadavg這個double數組将會存放負載值的結果,nelems決定樣本數量,nelems隻能取值為1到3,分别代表最近1、5、15分鐘内系統的平均負載。如果無法擷取系統的負載,此方法傳回-1,否則傳回擷取到的樣本數量(loadavg中有效的元素個數)。實驗中這個方法一直傳回-1,其實完全可以使用JMX中的相關方法替代此方法。
<code>public native void throwException(Throwable ee);</code>
繞過檢測機制直接抛出異常。
輸出結果:
輸出結果說明了staticFieldOffset隻能使用在靜态屬性,objectFieldOffset隻能使用在非靜态屬性。
擷取類中的靜态屬性值,隻依賴到Field的執行個體,剩餘工作交給Unsafe的API。
這個是JDK中使用直接記憶體的Buffer。可以檢視它的構造函數如下:
如果你不想下載下傳openjdk的源碼,下面貼出Unsafe類的源碼,來自openjdk-8u40-src-b25-10_feb_2015:
技術公衆号(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):
娛樂公衆号(《天天沙雕》),甄選奇趣沙雕圖文和視訊不定期推送,緩解生活工作壓力:
