Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操做的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提高Java運行效率、加強Java語言底層資源操做能力方面起到了很大的做用。但因爲Unsafe類使Java語言擁有了相似C語言指針同樣操做內存空間的能力,這無疑也增長了程序發生相關指針問題的風險。在程序中過分、不正確使用Unsafe類會使得程序出錯的機率變大,使得Java這種安全的語言變得再也不「安全」,所以對Unsafe的使用必定要慎重。html
注:本文對sun.misc.Unsafe公共API功能及相關應用場景進行介紹。java
以下Unsafe源碼所示,Unsafe類爲一單例實現,提供靜態方法getUnsafe獲取Unsafe實例,當且僅當調用getUnsafe方法的類爲引導類加載器所加載時才合法,不然拋出SecurityException異常。算法
public final class Unsafe { // 單例對象 private static final Unsafe theUnsafe; private Unsafe() { } @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); // 僅在引導類加載器`BootstrapClassLoader`加載時才合法 if(!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } } }
那如若想使用這個類,該如何獲取其實例?有以下兩個可行方案。bootstrap
其一,從getUnsafe
方法的使用限制條件出發,經過Java命令行命令-Xbootclasspath/a
把調用Unsafe相關方法的類A所在jar包路徑追加到默認的bootstrap路徑中,使得A被引導類加載器加載,從而經過Unsafe.getUnsafe
方法安全的獲取Unsafe實例。後端
java -Xbootclasspath/a: ${path} // 其中path爲調用Unsafe相關方法的類所在jar包路徑
其二,經過反射獲取單例對象theUnsafe。數組
private static Unsafe reflectGetUnsafe() { try { Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); return (Unsafe) field.get(null); } catch (Exception e) { log.error(e.getMessage(), e); return null; } }
如上圖所示,Unsafe提供的API大體可分爲內存操做、CAS、Class相關、對象操做、線程調度、系統信息獲取、內存屏障、數組操做等幾類,下面將對其相關方法和應用場景進行詳細介紹。安全
這部分主要包含堆外內存的分配、拷貝、釋放、給定地址值操做等方法。bash
//分配內存, 至關於C++的malloc函數 public native long allocateMemory(long bytes); //擴充內存 public native long reallocateMemory(long address, long bytes); //釋放內存 public native void freeMemory(long address); //在給定的內存塊中設置值 public native void setMemory(Object o, long offset, long bytes, byte value); //內存拷貝 public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); //獲取給定地址值,忽略修飾限定符的訪問限制。與此相似操做還有: getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); //爲給定地址設置值,忽略修飾限定符的訪問限制,與此相似操做還有: putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); //獲取給定地址的byte類型的值(當且僅當該內存地址爲allocateMemory分配時,此方法結果爲肯定的) public native byte getByte(long address); //爲給定地址設置byte類型的值(當且僅當該內存地址爲allocateMemory分配時,此方法結果纔是肯定的) public native void putByte(long address, byte x);
一般,咱們在Java中建立的對象都處於堆內內存(heap)中,堆內內存是由JVM所管控的Java進程內存,而且它們遵循JVM的內存管理機制,JVM會採用垃圾回收機制統一管理堆內存。與之相對的是堆外內存,存在於JVM管控以外的內存區域,Java中對堆外內存的操做,依賴於Unsafe提供的操做堆外內存的native方法。微信
DirectByteBuffer是Java用於實現堆外內存的一個重要類,一般用在通訊過程當中作緩衝池,如在Netty、MINA等NIO框架中應用普遍。DirectByteBuffer對於堆外內存的建立、使用、銷燬等邏輯均由Unsafe提供的堆外內存API來實現。併發
下圖爲DirectByteBuffer構造函數,建立DirectByteBuffer的時候,經過Unsafe.allocateMemory分配內存、Unsafe.setMemory進行內存初始化,然後構建Cleaner對象用於跟蹤DirectByteBuffer對象的垃圾回收,以實現當DirectByteBuffer被垃圾回收時,分配的堆外內存一塊兒被釋放。
那麼如何經過構建垃圾回收追蹤對象Cleaner實現堆外內存釋放呢?
Cleaner繼承自Java四大引用類型之一的虛引用PhantomReference(衆所周知,沒法經過虛引用獲取與之關聯的對象實例,且當對象僅被虛引用引用時,在任何發生GC的時候,其都可被回收),一般PhantomReference與引用隊列ReferenceQueue結合使用,能夠實現虛引用關聯對象被垃圾回收時可以進行系統通知、資源清理等功能。以下圖所示,當某個被Cleaner引用的對象將被回收時,JVM垃圾收集器會將此對象的引用放入到對象引用中的pending鏈表中,等待Reference-Handler進行相關處理。其中,Reference-Handler爲一個擁有最高優先級的守護線程,會循環不斷的處理pending鏈表中的對象引用,執行Cleaner的clean方法進行相關清理工做。
因此當DirectByteBuffer僅被Cleaner引用(即爲虛引用)時,其能夠在任意GC時段被回收。當DirectByteBuffer實例對象被回收時,在Reference-Handler線程操做中,會調用Cleaner的clean方法根據建立Cleaner時傳入的Deallocator來進行堆外內存的釋放。
以下源代碼釋義所示,這部分主要爲CAS相關操做的方法。
/** * CAS * @param o 包含要修改field的對象 * @param offset 對象中某field的偏移量 * @param expected 指望值 * @param update 更新值 * @return true | false */ public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
什麼是CAS? 即比較並替換,實現併發算法時經常使用到的一種技術。CAS操做包含三個操做數——內存位置、預期原值及新值。執行CAS操做的時候,將內存位置的值與預期原值比較,若是相匹配,那麼處理器會自動將該位置值更新爲新值,不然,處理器不作任何操做。咱們都知道,CAS是一條CPU的原子指令(cmpxchg指令),不會形成所謂的數據不一致問題,Unsafe提供的CAS方法(如compareAndSwapXXX)底層實現即爲CPU指令cmpxchg。
CAS在java.util.concurrent.atomic相關類、Java AQS、CurrentHashMap等實現上有很是普遍的應用。以下圖所示,AtomicInteger的實現中,靜態字段valueOffset即爲字段value的內存偏移地址,valueOffset的值在AtomicInteger初始化時,在靜態代碼塊中經過Unsafe的objectFieldOffset方法獲取。在AtomicInteger中提供的線程安全方法中,經過字段valueOffset的值能夠定位到AtomicInteger對象中value的內存地址,從而能夠根據CAS實現對value字段的原子操做。
下圖爲某個AtomicInteger對象自增操做先後的內存示意圖,對象的基地址baseAddress="0x110000",經過baseAddress+valueOffset獲得value的內存地址valueAddress="0x11000c";而後經過CAS進行原子性的更新操做,成功則返回,不然繼續重試,直到更新成功爲止。
這部分,包括線程掛起、恢復、鎖機制等方法。
//取消阻塞線程 public native void unpark(Object thread); //阻塞線程 public native void park(boolean isAbsolute, long time); //得到對象鎖(可重入鎖) @Deprecated public native void monitorEnter(Object o); //釋放對象鎖 @Deprecated public native void monitorExit(Object o); //嘗試獲取對象鎖 @Deprecated public native boolean tryMonitorEnter(Object o);
如上源碼說明中,方法park、unpark便可實現線程的掛起與恢復,將一個線程進行掛起是經過park方法實現的,調用park方法後,線程將一直阻塞直到超時或者中斷等條件出現;unpark能夠終止一個掛起的線程,使其恢復正常。
Java鎖和同步器框架的核心類AbstractQueuedSynchronizer,就是經過調用LockSupport.park()
和LockSupport.unpark()
實現線程的阻塞和喚醒的,而LockSupport的park、unpark方法實際是調用Unsafe的park、unpark方式來實現。
此部分主要提供Class和它的靜態字段的操做相關方法,包含靜態字段內存定位、定義類、定義匿名類、檢驗&確保初始化等。
//獲取給定靜態字段的內存地址偏移量,這個值對於給定的字段是惟一且固定不變的 public native long staticFieldOffset(Field f); //獲取一個靜態類中給定字段的對象指針 public native Object staticFieldBase(Field f); //判斷是否須要初始化一個類,一般在獲取一個類的靜態屬性的時候(由於一個類若是沒初始化,它的靜態屬性也不會初始化)使用。 當且僅當ensureClassInitialized方法不生效時返回false。 public native boolean shouldBeInitialized(Class<?> c); //檢測給定的類是否已經初始化。一般在獲取一個類的靜態屬性的時候(由於一個類若是沒初始化,它的靜態屬性也不會初始化)使用。 public native void ensureClassInitialized(Class<?> c); //定義一個類,此方法會跳過JVM的全部安全檢查,默認狀況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例來源於調用者 public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); //定義一個匿名類 public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
從Java 8開始,JDK使用invokedynamic及VM Anonymous Class結合來實現Java語言層面上的Lambda表達式。
在Lambda表達式實現中,經過invokedynamic指令調用引導方法生成調用點,在此過程當中,會經過ASM動態生成字節碼,然後利用Unsafe的defineAnonymousClass方法定義實現相應的函數式接口的匿名類,而後再實例化此匿名類,並返回與此匿名類中函數式方法的方法句柄關聯的調用點;然後能夠經過此調用點實現調用相應Lambda表達式定義邏輯的功能。下面以以下圖所示的Test類來舉例說明。
Test類編譯後的class文件反編譯後的結果以下圖一所示(刪除了對本文說明無心義的部分),咱們能夠從中看到main方法的指令實現、invokedynamic指令調用的引導方法BootstrapMethods、及靜態方法lambda$main$0
(實現了Lambda表達式中字符串打印邏輯)等。在引導方法執行過程當中,會經過Unsafe.defineAnonymousClass生成以下圖二所示的實現Consumer接口的匿名類。其中,accept方法經過調用Test類中的靜態方法lambda$main$0
來實現Lambda表達式中定義的邏輯。然後執行語句consumer.accept("lambda")
其實就是調用下圖二所示的匿名類的accept方法。
此部分主要包含對象成員屬性相關操做及很是規的對象實例化方式等相關方法。
//返回對象成員屬性在內存地址相對於此對象的內存地址的偏移量 public native long objectFieldOffset(Field f); //得到給定對象的指定地址偏移量的值,與此相似操做還有:getInt,getDouble,getLong,getChar等 public native Object getObject(Object o, long offset); //給定對象的指定地址偏移量設值,與此相似操做還有:putInt,putDouble,putLong,putChar等 public native void putObject(Object o, long offset, Object x); //從對象的指定偏移量處獲取變量的引用,使用volatile的加載語義 public native Object getObjectVolatile(Object o, long offset); //存儲變量的引用到對象的指定的偏移量處,使用volatile的存儲語義 public native void putObjectVolatile(Object o, long offset, Object x); //有序、延遲版本的putObjectVolatile方法,不保證值的改變被其餘線程當即看到。只有在field被volatile修飾符修飾時有效 public native void putOrderedObject(Object o, long offset, Object x); //繞過構造方法、初始化代碼來建立對象 public native Object allocateInstance(Class<?> cls) throws InstantiationException;
以下圖所示,在Gson反序列化時,若是類有默認構造函數,則經過反射調用默認構造函數建立實例,不然經過UnsafeAllocator來實現對象實例的構造,UnsafeAllocator經過調用Unsafe的allocateInstance實現對象的實例化,保證在目標類無默認構造函數時,反序列化不夠影響。
這部分主要介紹與數據操做相關的arrayBaseOffset與arrayIndexScale這兩個方法,二者配合起來使用,便可定位數組中每一個元素在內存中的位置。
//返回數組中第一個元素的偏移地址 public native int arrayBaseOffset(Class<?> arrayClass); //返回數組中一個元素佔用的大小 public native int arrayIndexScale(Class<?> arrayClass);
這兩個與數據操做相關的方法,在java.util.concurrent.atomic 包下的AtomicIntegerArray(能夠實現對Integer數組中每一個元素的原子性操做)中有典型的應用,以下圖AtomicIntegerArray源碼所示,經過Unsafe的arrayBaseOffset、arrayIndexScale分別獲取數組首元素的偏移地址base及單個元素大小因子scale。後續相關原子性操做,均依賴於這兩個值進行數組中元素的定位,以下圖二所示的getAndAdd方法即經過checkedByteOffset方法獲取某數組元素的偏移地址,然後經過CAS實現原子性操做。
在Java 8中引入,用於定義內存屏障(也稱內存柵欄,內存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操做中的一個同步點,使得此點以前的全部讀寫操做都執行後才能夠開始執行此點以後的操做),避免代碼重排序。
//內存屏障,禁止load操做重排序。屏障前的load操做不能被重排序到屏障後,屏障後的load操做不能被重排序到屏障前 public native void loadFence(); //內存屏障,禁止store操做重排序。屏障前的store操做不能被重排序到屏障後,屏障後的store操做不能被重排序到屏障前 public native void storeFence(); //內存屏障,禁止load、store操做重排序 public native void fullFence();
在Java 8中引入了一種鎖的新機制——StampedLock,它能夠當作是讀寫鎖的一個改進版本。StampedLock提供了一種樂觀讀鎖的實現,這種樂觀讀鎖相似於無鎖的操做,徹底不會阻塞寫線程獲取寫鎖,從而緩解讀多寫少時寫線程「飢餓」現象。因爲StampedLock提供的樂觀讀鎖不阻塞寫線程獲取讀鎖,當線程共享變量從主內存load到線程工做內存時,會存在數據不一致問題,因此當使用StampedLock的樂觀讀鎖時,須要聽從以下圖用例中使用的模式來確保數據的一致性。
如上圖用例所示計算座標點Point對象,包含點移動方法move及計算此點到原點的距離的方法distanceFromOrigin。在方法distanceFromOrigin中,首先,經過tryOptimisticRead方法獲取樂觀讀標記;而後從主內存中加載點的座標值 (x,y);然後經過StampedLock的validate方法校驗鎖狀態,判斷座標點(x,y)從主內存加載到線程工做內存過程當中,主內存的值是否已被其餘線程經過move方法修改,若是validate返回值爲true,證實(x, y)的值未被修改,可參與後續計算;不然,需加悲觀讀鎖,再次從主內存加載(x,y)的最新值,而後再進行距離計算。其中,校驗鎖狀態這步操做相當重要,須要判斷鎖狀態是否發生改變,從而判斷以前copy到線程工做內存中的值是否與主內存的值存在不一致。
下圖爲StampedLock.validate方法的源碼實現,經過鎖標記與相關常量進行位運算、比較來校驗鎖狀態,在校驗邏輯以前,會經過Unsafe的loadFence方法加入一個load內存屏障,目的是避免上圖用例中步驟②和StampedLock.validate中鎖狀態校驗運算髮生重排序致使鎖狀態校驗不許確的問題。
這部分包含兩個獲取系統相關信息的方法。
//返回系統指針的大小。返回值爲4(32位系統)或 8(64位系統)。 public native int addressSize(); //內存頁的大小,此值爲2的冪次方。 public native int pageSize();
以下圖所示的代碼片斷,爲java.nio下的工具類Bits中計算待申請內存所需內存頁數量的靜態方法,其依賴於Unsafe中pageSize方法獲取系統內存頁大小實現後續計算邏輯。
本文對Java中的sun.misc.Unsafe的用法及應用場景進行了基本介紹,咱們能夠看到Unsafe提供了不少便捷、有趣的API方法。即使如此,因爲Unsafe中包含大量自主操做內存的方法,如若使用不當,會對程序帶來許多不可控的災難。所以對它的使用咱們須要慎之又慎。
璐璐,美團點評Java開發工程師。2017年加入美團點評,負責美團點評境內度假的後端開發。
歡迎加入美團Java技術交流羣,跟做者零距離交流。進羣方式:請加美美同窗微信(微信號:MTDPtech02),回覆:Java,美美會自動拉你進羣。