Unsafe實現原理與Unsafe應用

前言

Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升Java運行效率、增強Java語言底層資源操作能力方面起到了很大的作用。但由於Unsafe類使Java語言擁有了類似C語言指針一樣操作內存空間的能力,這無疑也增加了程序發生相關指針問題的風險。在程序中過度、不正確使用Unsafe類會使得程序出錯的概率變大,使得Java這種安全的語言變得不再“安全”,因此對Unsafe的使用一定要慎重。

注:本文對sun.misc.Unsafe公共API功能及相關應用場景進行介紹。

基本介紹

如下Unsafe源碼所示,Unsafe類爲一單例實現,提供靜態方法getUnsafe獲取Unsafe實例,當且僅當調用getUnsafe方法的類爲引導類加載器所加載時才合法,否則拋出SecurityException異常。

  1. public final class Unsafe {
  2. // 單例對象
  3. private static final Unsafe theUnsafe;
  4. private Unsafe() {
  5. }
  6. @CallerSensitive
  7. public static Unsafe getUnsafe() {
  8. Class var0 = Reflection.getCallerClass();
  9. // 僅在引導類加載器`BootstrapClassLoader`加載時才合法
  10. if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
  11. throw new SecurityException("Unsafe");
  12. } else {
  13. return theUnsafe;
  14. }
  15. }
  16. }

那如若想使用這個類,該如何獲取其實例?有如下兩個可行方案。

其一,從getUnsafe方法的使用限制條件出發,通過Java命令行命令-Xbootclasspath/a把調用Unsafe相關方法的類A所在jar包路徑追加到默認的bootstrap路徑中,使得A被引導類加載器加載,從而通過Unsafe.getUnsafe方法安全的獲取Unsafe實例。

java -Xbootclasspath/a: ${path}   // 其中path爲調用Unsafe相關方法的類所在jar包路徑 

其二,通過反射獲取單例對象theUnsafe。

  1. private static Unsafe reflectGetUnsafe() {
  2. try {
  3. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  4. field.setAccessible(true);
  5. return (Unsafe) field.get(null);
  6. } catch (Exception e) {
  7. log.error(e.getMessage(), e);
  8. return null;
  9. }
  10. }

功能介紹

如上圖所示,Unsafe提供的API大致可分爲內存操作、CAS、Class相關、對象操作、線程調度、系統信息獲取、內存屏障、數組操作等幾類,下面將對其相關方法和應用場景進行詳細介紹。

Unsafe類的成員

  • 除了上面談及的getUnsafe會返回Unsafe實例theUnsafe外,Unsafe一共由105個方法組成,大部分都是native方法。下面是一些可能用到的方法:
  1. 返回低級別內存信息

addressSize()
pageSize()

  1. 手動獲得對象和對象方法

allocateInstance() 避開構造方法生成對象
objectFieldOffset() 獲得對象的某個成員的地址偏移量

  1. 手動獲得類或者靜態成員

staticFieldOffset() 獲得某個靜態成員的地址偏移量
defineClass()
defineAnonymousClass()
ensureClassInitialized()

  1. 手動獲得數組

arrayBaseOffset()
arrayIndexScale()

  1. 同步的低級別基本方法

monitorEnter()
tryMonitorEnter()
monitorExit()
compareAndSwapInt()
putOrderedInt()

  1. 手動操作內存

allocateMemory()
copyMemory()
freeMemory()
getAddress()
getInt() ,getInt(Object var1, long var2)第一個參數是要get的對象,第二個參數是字段的偏移量
putInt()

  1. 阻塞和喚醒

pack()
unpack()

Unsafe常用方式

  • 避開構造方法初始化對象,使用allocateInstance

 

  1. Unsafe unsafe = getUnsafe();
  2. final Class aClass = A.class;
  3. A a = (A) unsafe.allocateInstance(aClass);
  • 修改對象成員值(內存出錯),使用putInt()

 

  1. A a = new A(12);
  2. Field f = A.class.getDeclaredField("num");
  3. unsafe.putInt(a, unsafe.objectFieldOffset(f), 8);
  • 淺複製

 

  1. /**
  2. * 將對象轉化成地址
  3. * @param obj
  4. * @return
  5. */
  6. private static long toAddress(Object obj) {
  7. Object[] objects = new Object[]{obj};
  8. long baseOffset = getUnsafe().arrayBaseOffset(objects.getClass());
  9. return normalize(getUnsafe().getInt(objects, baseOffset));
  10. }
  11. /**
  12. * 將地址轉化成對象
  13. * @param address
  14. * @return
  15. */
  16. private static Object fromAddress(long address) {
  17. Object[] objects = new Object[]{null};
  18. long baseOffset = getUnsafe().arrayBaseOffset(objects.getClass());
  19. getUnsafe().putLong(objects, baseOffset, address);
  20. return objects[0];
  21. }
  22. private static long normalize(int value) {
  23. if (value > 0) {
  24. return value;
  25. }
  26. return (~0L >>> 32) & value;
  27. }
  28. public static Object shallowCopy(Object obj) {
  29. long size = sizeOf(obj);//對象所需內存大小
  30. long start = toAddress(obj); //對象的地址起始偏移量
  31. long address = getUnsafe().allocateMemory(size);//分配size大小的內存,返回內存空間地址偏移量,準備放入複製的對象
  32. getUnsafe().copyMemory(start, address, size);//從對象地址起始偏移量開始複製內存到address開始的內存
  33. return fromAddress(address);//將地址轉化成對象
  34. }
  • 實現多繼承,將兩個對象的內存空間合併,得到地址轉換成對象;
  • 動態生成類,動態代理庫cglib的原理,使用defineClass方法;
  • 拋出異常:unsafe.throwException(new IOException());
  • 快速序列化和反序列化:
    序列化:使用getLong, getInt, getObject等方法;
    反序列化:首先使用allocateInstance生成對象,然後使用putLong, putInt, putObject等方法,填充對象;
  • 併發操作:CAS(compareAndSwap)的方法包括

 

  1. compareAndSwapObject(Object obj, long offset, Object expect, Object update);
  2. compareAndSwapInt(Object obj, long offset, int expect, int update);
  3. compareAndSwapLong(Object obj, long offset, long expect, long update);

CAS是原子操作,能夠用於實現高性能的線程安全的無鎖數據結構,對沒錯,atomic包內的原子類都是實現的CAS,下面會詳細分析。

內存操作

這部分主要包含堆外內存的分配、拷貝、釋放、給定地址值操作等方法。

  1. //分配內存, 相當於C++的malloc函數
  2. public native long allocateMemory(long bytes);
  3. //擴充內存
  4. public native long reallocateMemory(long address, long bytes);
  5. //釋放內存
  6. public native void freeMemory(long address);
  7. //在給定的內存塊中設置值
  8. public native void setMemory(Object o, long offset, long bytes, byte value);
  9. //內存拷貝
  10. public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
  11. //獲取給定地址值,忽略修飾限定符的訪問限制。與此類似操作還有: getInt,getDouble,getLong,getChar等
  12. public native Object getObject(Object o, long offset);
  13. //爲給定地址設置值,忽略修飾限定符的訪問限制,與此類似操作還有: putInt,putDouble,putLong,putChar等
  14. public native void putObject(Object o, long offset, Object x);
  15. //獲取給定地址的byte類型的值(當且僅當該內存地址爲allocateMemory分配時,此方法結果爲確定的)
  16. public native byte getByte(long address);
  17. //爲給定地址設置byte類型的值(當且僅當該內存地址爲allocateMemory分配時,此方法結果纔是確定的)
  18. public native void putByte(long address, byte x);

通常,我們在Java中創建的對象都處於堆內內存(heap)中,堆內內存是由JVM所管控的Java進程內存,並且它們遵循JVM的內存管理機制,JVM會採用垃圾回收機制統一管理堆內存。與之相對的是堆外內存,存在於JVM管控之外的內存區域,Java中對堆外內存的操作,依賴於Unsafe提供的操作堆外內存的native方法。

使用堆外內存的原因

  • 對垃圾回收停頓的改善。由於堆外內存是直接受操作系統管理而不是JVM,所以當我們使用堆外內存時,即可保持較小的堆內內存規模。從而在GC時減少回收停頓對於應用的影響。
  • 提升程序I/O操作的性能。通常在I/O通信過程中,會存在堆內內存到堆外內存的數據拷貝操作,對於需要頻繁進行內存間數據拷貝且生命週期較短的暫存數據,都建議存儲到堆外內存。

典型應用

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相關操作的方法。

  1. /**
  2. * CAS
  3. * @param o 包含要修改field的對象
  4. * @param offset 對象中某field的偏移量
  5. * @param expected 期望值
  6. * @param update 更新值
  7. * @return true | false
  8. */
  9. public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
  10. public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
  11. 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進行原子性的更新操作,成功則返回,否則繼續重試,直到更新成功爲止。

線程調度

這部分,包括線程掛起、恢復、鎖機制等方法。

  1. //取消阻塞線程
  2. public native void unpark(Object thread);
  3. //阻塞線程
  4. public native void park(boolean isAbsolute, long time);
  5. //獲得對象鎖(可重入鎖)
  6. @Deprecated
  7. public native void monitorEnter(Object o);
  8. //釋放對象鎖
  9. @Deprecated
  10. public native void monitorExit(Object o);
  11. //嘗試獲取對象鎖
  12. @Deprecated
  13. public native boolean tryMonitorEnter(Object o);

如上源碼說明中,方法park、unpark即可實現線程的掛起與恢復,將一個線程進行掛起是通過park方法實現的,調用park方法後,線程將一直阻塞直到超時或者中斷等條件出現;unpark可以終止一個掛起的線程,使其恢復正常。

典型應用

Java鎖和同步器框架的核心類AbstractQueuedSynchronizer,就是通過調用LockSupport.park()LockSupport.unpark()實現線程的阻塞和喚醒的,而LockSupport的park、unpark方法實際是調用Unsafe的park、unpark方式來實現。

Class相關

此部分主要提供Class和它的靜態字段的操作相關方法,包含靜態字段內存定位、定義類、定義匿名類、檢驗&確保初始化等。

  1. //獲取給定靜態字段的內存地址偏移量,這個值對於給定的字段是唯一且固定不變的
  2. public native long staticFieldOffset(Field f);
  3. //獲取一個靜態類中給定字段的對象指針
  4. public native Object staticFieldBase(Field f);
  5. //判斷是否需要初始化一個類,通常在獲取一個類的靜態屬性的時候(因爲一個類如果沒初始化,它的靜態屬性也不會初始化)使用。 當且僅當ensureClassInitialized方法不生效時返回false。
  6. public native boolean shouldBeInitialized(Class<?> c);
  7. //檢測給定的類是否已經初始化。通常在獲取一個類的靜態屬性的時候(因爲一個類如果沒初始化,它的靜態屬性也不會初始化)使用。
  8. public native void ensureClassInitialized(Class<?> c);
  9. //定義一個類,此方法會跳過JVM的所有安全檢查,默認情況下,ClassLoader(類加載器)和ProtectionDomain(保護域)實例來源於調用者
  10. public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
  11. //定義一個匿名類
  12. public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

典型應用

從Java 8開始,JDK使用invokedynamic及VM Anonymous Class結合來實現Java語言層面上的Lambda表達式。

  • invokedynamic: invokedynamic是Java 7爲了實現在JVM上運行動態語言而引入的一條新的虛擬機指令,它可以實現在運行期動態解析出調用點限定符所引用的方法,然後再執行該方法,invokedynamic指令的分派邏輯是由用戶設定的引導方法決定。
  • VM Anonymous Class:可以看做是一種模板機制,針對於程序動態生成很多結構相同、僅若干常量不同的類時,可以先創建包含常量佔位符的模板類,而後通過Unsafe.defineAnonymousClass方法定義具體類時填充模板的佔位符生成具體的匿名類。生成的匿名類不顯式掛在任何ClassLoader下面,只要當該類沒有存在的實例對象、且沒有強引用來引用該類的Class對象時,該類就會被GC回收。故而VM Anonymous Class相比於Java語言層面的匿名內部類無需通過ClassClassLoader進行類加載且更易回收。

在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方法。

對象操作

此部分主要包含對象成員屬性相關操作及非常規的對象實例化方式等相關方法。

  1. //返回對象成員屬性在內存地址相對於此對象的內存地址的偏移量
  2. public native long objectFieldOffset(Field f);
  3. //獲得給定對象的指定地址偏移量的值,與此類似操作還有:getInt,getDouble,getLong,getChar等
  4. public native Object getObject(Object o, long offset);
  5. //給定對象的指定地址偏移量設值,與此類似操作還有:putInt,putDouble,putLong,putChar等
  6. public native void putObject(Object o, long offset, Object x);
  7. //從對象的指定偏移量處獲取變量的引用,使用volatile的加載語義
  8. public native Object getObjectVolatile(Object o, long offset);
  9. //存儲變量的引用到對象的指定的偏移量處,使用volatile的存儲語義
  10. public native void putObjectVolatile(Object o, long offset, Object x);
  11. //有序、延遲版本的putObjectVolatile方法,不保證值的改變被其他線程立即看到。只有在field被volatile修飾符修飾時有效
  12. public native void putOrderedObject(Object o, long offset, Object x);
  13. //繞過構造方法、初始化代碼來創建對象
  14. public native Object allocateInstance(Class<?> cls) throws InstantiationException;

典型應用

  • 常規對象實例化方式:我們通常所用到的創建對象的方式,從本質上來講,都是通過new機制來實現對象的創建。但是,new機制有個特點就是當類只提供有參的構造函數且無顯示聲明無參構造函數時,則必須使用有參構造函數進行對象構造,而使用有參構造函數時,必須傳遞相應個數的參數才能完成對象實例化。
  • 非常規的實例化方式:而Unsafe中提供allocateInstance方法,僅通過Class對象就可以創建此類的實例對象,而且不需要調用其構造函數、初始化代碼、JVM安全檢查等。它抑制修飾符檢測,也就是即使構造器是private修飾的也能通過此方法實例化,只需提類對象即可創建相應的對象。由於這種特性,allocateInstance在java.lang.invoke、Objenesis(提供繞過類構造器的對象生成方式)、Gson(反序列化時用到)中都有相應的應用。

如下圖所示,在Gson反序列化時,如果類有默認構造函數,則通過反射調用默認構造函數創建實例,否則通過UnsafeAllocator來實現對象實例的構造,UnsafeAllocator通過調用Unsafe的allocateInstance實現對象的實例化,保證在目標類無默認構造函數時,反序列化不夠影響。

數組相關

這部分主要介紹與數據操作相關的arrayBaseOffset與arrayIndexScale這兩個方法,兩者配合起來使用,即可定位數組中每個元素在內存中的位置。

  1. //返回數組中第一個元素的偏移地址
  2. public native int arrayBaseOffset(Class<?> arrayClass);
  3. //返回數組中一個元素佔用的大小
  4. public native int arrayIndexScale(Class<?> arrayClass);

典型應用

這兩個與數據操作相關的方法,在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以實現對Integer數組中每個元素的原子性操作)中有典型的應用,如下圖AtomicIntegerArray源碼所示,通過Unsafe的arrayBaseOffset、arrayIndexScale分別獲取數組首元素的偏移地址base及單個元素大小因子scale。後續相關原子性操作,均依賴於這兩個值進行數組中元素的定位,如下圖二所示的getAndAdd方法即通過checkedByteOffset方法獲取某數組元素的偏移地址,而後通過CAS實現原子性操作。

內存屏障

在Java 8中引入,用於定義內存屏障(也稱內存柵欄,內存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行後纔可以開始執行此點之後的操作),避免代碼重排序。

  1. //內存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障後,屏障後的load操作不能被重排序到屏障前
  2. public native void loadFence();
  3. //內存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障後,屏障後的store操作不能被重排序到屏障前
  4. public native void storeFence();
  5. //內存屏障,禁止load、store操作重排序
  6. 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中鎖狀態校驗運算髮生重排序導致鎖狀態校驗不準確的問題。

系統相關

這部分包含兩個獲取系統相關信息的方法。

  1. //返回系統指針的大小。返回值爲4(32位系統)或 8(64位系統)。
  2. public native int addressSize();
  3. //內存頁的大小,此值爲2的冪次方。
  4. public native int pageSize();

典型應用

如下圖所示的代碼片段,爲java.nio下的工具類Bits中計算待申請內存所需內存頁數量的靜態方法,其依賴於Unsafe中pageSize方法獲取系統內存頁大小實現後續計算邏輯。

結語

本文對Java中的sun.misc.Unsafe的用法及應用場景進行了基本介紹,我們可以看到Unsafe提供了很多便捷、有趣的API方法。即便如此,由於Unsafe中包含大量自主操作內存的方法,如若使用不當,會對程序帶來許多不可控的災難。因此對它的使用我們需要慎之又慎。

參考資料

作者簡介

  • 璐璐,美團點評Java開發工程師。2017年加入美團點評,負責美團點評境內度假的後端開發。

 

筆者的微信公衆號,每天一篇好文章:

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