詳解Unsafe類

安全劍客發表於2020-12-15
如果你學習了一些java併發包裡面的類原始碼的話,對這個Unsafe類一定不陌生,整個java併發包底層實現的核心就是它,在很久之前盛傳著這個類將要在jdk9移除,事實上如果移除了那麼一大批框架將會消失,比如說赫赫有名的Netty框架。

可能我們會奇怪,java中竟然給一個類起名字叫做“不安全”。慢慢看,你就會發現這個類的神奇之處,雖然功能很強大,但是的確不那麼安全。

一、簡單介紹

首先在Oracle的Jdk8無法獲取到sun.misc包的原始碼,想看此包的原始碼可以直接下載openjdk。

1、預備工作

openjdk的原始碼我下載的是openjdk-8u40-src-b25-10_feb_2015,有需要的可以私信我,如果是我公眾號粉絲,我會直接附加上這個百度雲資源。在下載完成之後,然後就可以直接匯入我們的eclipse了。

詳解Unsafe類詳解Unsafe類

windows->preference->installed jres->選中jre->edit->rt.jar->source attachment->external folders->openjdk原始碼路徑。此時就可以檢視我們的Unsafe類的原始碼了。

2、簡介說明

如果你學習了一些java併發包裡面的類原始碼的話,對這個Unsafe類一定不陌生,整個java併發包底層實現的核心就是它,在很久之前盛傳著這個類將要在jdk9移除,事實上如果移除了那麼一大批框架將會消失,比如說赫赫有名的Netty框架。最終jdk9出現的時候也只是對其進行了改進和最佳化。不過這也再一次說明了這個類的重要地位。

為什麼說它一半是天使一半是魔鬼呢?要回答這個問題,我們還是要從其特性來解釋。

(1)Unsafe類使Java擁有了像C語言的指標一樣操作記憶體空間的能力,一旦能夠直接操作記憶體,這也就意味著(1)不受jvm管理,也就意味著無法被GC,需要我們手動GC,稍有不慎就會出現記憶體洩漏。

(2)Unsafe的不少方法中必須提供原始地址(記憶體地址)和被替換物件的地址,偏移量要自己計算,一旦出現問題就是JVM崩潰級別的異常,會導致整個JVM例項崩潰,表現為應用程式直接crash掉。

(3)直接操作記憶體,也意味著其速度更快,在高併發的條件之下能夠很好地提高效率。

因此,從上面三個角度來看,雖然在一定程度上提升了效率但是也帶來了指標的不安全性。

下面我們深入到原始碼中看看,提供了什麼方法直接操作記憶體。

二、原始碼分析

Unsafe中一共有82個public native修飾的方法,還有幾十個基於這82個public native方法的其他方法。這些方法大體可以歸結為以下幾類:

  1. (1)初始化操作
  1. (2)操作物件屬性
  1. (3)運算元組元素
  1. (4)記憶體管理
  1. (5)記憶體屏障
  1. (6)執行緒掛起和回覆
  1. (7)CAS機制

下面我們對這些方法盡進行一個大致的分析。

1、初始化
//1、註冊native方法,是的Unsafe類可以操作C語言 
 private static native void registerNatives(); 
 static { 
     registerNatives(); 
     sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe"); 
 } 
 //2、構造方法 
 private Unsafe() {} 
 //3、初始化方法 
 private static final Unsafe theUnsafe = new Unsafe(); 
 //4、初始化方法實現 
 @CallerSensitive 
 public static Unsafe getUnsafe() { 
     Class<?> caller = Reflection.getCallerClass(); 
     if (!VM.isSystemDomainLoader(caller.getClassLoader())) 
         throw new SecurityException("Unsafe"); 
     return theUnsafe; 
 }

在這裡我們看到Unsafe的初始化方法主要是透過getUnsafe方法的單例模式實現的,呼叫JVM本地方法registerNatives()和sun.reflect.Reflection,透過Reflection的getCallerClass判斷當前呼叫的類是否是主類載入器(BootStrap classLoader)載入的,否則的話丟擲一個SecurityException。這也證明了一個問題,那就是隻有由主類載入器(BootStrap classLoader)載入的類才能呼叫這個類中的方法。

2、操作屬性方法
  1. (1)public native Object getObject(Object o, long offset);

透過給定的Java變數獲取引用值。這裡實際上是獲取一個Java物件o中,獲取偏移地址為offset的屬性的值,此方法可以突破修飾符的抑制,也就是無視private、protected和default修飾符。類似的方法有getInt、getDouble等等。同理還有putObject方法。

  1. (2)public native Object getObjectVolatile(Object o, long offset);

強制從主存中獲取屬性值。類似的方法有getIntVolatile、getDoubleVolatile等等。同理還有putObjectVolatile。

  1. (3)public native void putOrderedObject(Object o, long offset, Object x);

設定o物件中offset偏移地址offset對應的Object型field的值為指定值x。這是一個有序或者有延遲的putObjectVolatile方法,並且不保證值的改變被其他執行緒立即看到。只有在field被volatile修飾並且期望被修改的時候使用才會生效。類似的方法有putOrderedInt和putOrderedLong。

  1. (4)public native long staticFieldOffset(Field f);

返回給定的靜態屬性在它的類的儲存分配中的位置(偏移地址)。

  1. (5)public native long objectFieldOffset(Field f);

返回給定的非靜態屬性在它的類的儲存分配中的位置(偏移地址)。

  1. (6)public native Object staticFieldBase(Field f);

返回給定的靜態屬性的位置,配合staticFieldOffset方法使用。

3、運算元組
  1. (1)public native int arrayBaseOffset(Class arrayClass);

返回陣列型別的第一個元素的偏移地址(基礎偏移地址)。

  1. (2)public native int arrayIndexScale(Class arrayClass);

返回陣列中元素與元素之間的偏移地址的增量。

這兩個方法配合使用就可以定位到任何一個元素的地址。

4、記憶體管理
  1. (1)public native int addressSize();

獲取本地指標的大小(單位是byte),通常值為4或者8。常量ADDRESS_SIZE就是呼叫此方法。

  1. (2)public native int pageSize();

獲取本地記憶體的頁數,此值為2的冪次方。

  1. (3)public native long allocateMemory(long bytes);

分配一塊新的本地記憶體,透過bytes指定記憶體塊的大小(單位是byte),返回新開闢的記憶體的地址。

  1. (4)public native long reallocateMemory(long address, long bytes);

透過指定的記憶體地址address重新調整本地記憶體塊的大小,調整後的記憶體塊大小透過bytes指定(單位為byte)。

  1. (5)public native void setMemory(Object o, long offset, long bytes, byte value);

將給定記憶體塊中的所有位元組設定為固定值(通常是0)。

5、執行緒掛起和恢復
  1. (1)public native void unpark(Object thread);

釋放被park建立的在一個執行緒上的阻塞。由於其不安全性,因此必須保證執行緒是存活的。

  1. (2)public native void park(boolean isAbsolute, long time);

阻塞當前執行緒,一直等道unpark方法被呼叫。

6、記憶體屏障
  1. (1)public native void loadFence();

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

  1. (2)public native void storeFence();

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

  1. (3)public native void fullFence();

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

7、CAS機制
public final native boolean compareAndSwapObject( 
    Object o, long offset, Object expected, Object x); 
public final native boolean compareAndSwapInt( 
    Object o, long offset,int expected, int x); 
public final native boolean compareAndSwapLong( 
    Object o, long offset, long expected,long x);

這個Unsafe類其實是貫穿到整個java併發包體系中的,不管是你看原子包還是lock包底部都有這樣的一個類,我們需要記住的不是每一個方法,而是上面七類的標題。也就是具體有什麼功能。

三、使用

說了這麼久的原始碼在這裡才介紹其使用,是因為官方並不推薦我們使用,也就是說我們無法直接new出來一個Unsafe類出來,那我們該如何使用呢?在很久之前我曾寫過一篇介紹java反射機制的文章,沒錯就是這個反射機制,牛的不行。Unsafe就可以透過反射機制來獲取。

public class UnsafeTest { 
    public static void main(String[] args) throws Exception { 
        //這裡的theUnsafe就是我們原始碼中的那個theUnsafe 
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); 
        theUnsafe.setAccessible(true); 
        Unsafe unsafe = (Unsafe) theUnsafe.get(null); 
 
        //1、建立物件例項 
        Author author = (Author) unsafe.allocateInstance(Author.class); 
        //2、操作物件的屬性 
        Field ageField = Author.class.getDeclaredField("age"); 
        long fieldOffset = unsafe.objectFieldOffset(ageField); 
        //3、運算元組 
        String[] strings = new String[]{"1", "2", "3"}; 
        long i = unsafe.arrayBaseOffset(String[].class); 
        //4、操作記憶體 
        long address = unsafe.allocateMemory(8L);         
    } 
}

在這裡只是給出一些簡單的例子,其用法可以參照原始碼分析中那七個方向。

原文地址:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559985/viewspace-2742244/,如需轉載,請註明出處,否則將追究法律責任。

相關文章