Java 的 Unsafe 類

KevinOfNeu發表於2018-07-28

0. 概述

本文主要介紹一下 JDK 裡的核彈之 sun.misc.Unsage 類。這個類提供了更加底層的操作,主要是服務於 Java 的核心類庫。普通使用者一般情況下,並不會直接用到該類。

1. 獲取 Unsafe 例項

Unsafe 提供了 getUnsafe 方法,程式碼例項如下所示:

public class Main {
    public static void main(String[] args) {
        Unsafe.getUnsafe();
    }
}
複製程式碼

但是,會有如下報錯:

Exception in thread "main" java.lang.SecurityException: Unsafe
  at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
  at com.bendcap.java.jvm.unsafe.Main.main(Main.java:13)
複製程式碼

這是因為 Unsafe 類主要是 JDK 內部使用,並不提供給普通使用者呼叫,也就是其名字所暗示的那樣,這些操作不安全。

但是我們仍讓可以通過反射獲取到例項:

public static Unsafe createUnsafe() {
        try {
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            Field field = unsafeClass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            return unsafe;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
複製程式碼

2. 不呼叫構造方法方法建立物件

我們知道,JVM 在建立物件例項的時候會呼叫預設或者有參構造方法,在位元組碼中對應 init 方法,那如何例項話一個物件,而不呼叫 init 方法呢? 答案就是 Unsafe

假設我們有一個類 Person:

    private String name;
    private int age;

    static {
        System.out.println("Person static code block");
    }

    public Person() {
        System.out.println("Person default constructor");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
複製程式碼

那麼我們可以通過 Unsafe 在記憶體中直接建立 Person 的例項:

public Person instancePerson() {
        Person person = null;
        try {
            person = (Person) createUnsafe().allocateInstance(Person.class);
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        person.setName("Unsafer");
        return person;
    }
複製程式碼

Unsafe 只會分配 Person 對應的記憶體空間,而不觸發建構函式。 : Class 檔案中的 clinit 仍然會執行。

舉一個生產例項。Gson 中反序列化的時候,ReflectiveTypeAdapterFactory 類負責通過反射設定欄位值,其中在或許反序列化對應的 Class 例項的時候就用到了 Unsafe, 關鍵程式碼摘抄如下:

public abstract class UnsafeAllocator {
  public abstract <T> T newInstance(Class<T> c) throws Exception;

  public static UnsafeAllocator create() {
    // try JVM
    // public class Unsafe {
    //   public Object allocateInstance(Class<?> type);
    // }
    try {
      Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
      Field f = unsafeClass.getDeclaredField("theUnsafe");
      f.setAccessible(true);
      final Object unsafe = f.get(null);
      final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
      return new UnsafeAllocator() {
        @Override
        @SuppressWarnings("unchecked")
        public <T> T newInstance(Class<T> c) throws Exception {
          assertInstantiable(c);
          return (T) allocateInstance.invoke(unsafe, c);
        }
      };
    } catch (Exception ignored) {
    }
    ...
    // give up
    return new UnsafeAllocator() {
      @Override
      public <T> T newInstance(Class<T> c) {
        throw new UnsupportedOperationException("Cannot allocate " + c);
      }
    };
  }
  }
複製程式碼

3.改變私有欄位

假設我們有如下類:

public class SecretHolder {
    private int SECRET_VALUE = 0;

    public boolean secretValueDisclosed() {
        return SECRET_VALUE == 1;
    }
}
複製程式碼

下面我們通過 Unsafe 來改變私有屬性 SECRET_VALUE 的值。

 SecretHolder secretHolder = new SecretHolder();
        Field field = null;
        try {
            field = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
        Unsafe unsafe = createUnsafe();
        unsafe.putInt(secretHolder, unsafe.objectFieldOffset(field), 1);
        return secretHolder.secretValueDisclosed();
複製程式碼

我們通過 unsafe.putInt 直接改變了 SecretHolder 的私有屬性的值。一旦我們通過反射獲得了類的私有屬性欄位,我們就可以直接操作它的值。

4. 丟擲異常而不會觸發 CE(Checked Exception)

通過 unsafe.throwExceptio 建立的異常不會被編譯器檢查,方法的呼叫者也不需要處理異常。

 public void throwException() {
        Unsafe unsafe = createUnsafe();
        unsafe.throwException(new IOException());
    }
複製程式碼

5. 堆外記憶體

Java 中物件分配一般是在 Heap 中進行的(例外是 TLAB等),當應用記憶體不足的時候,可以通過觸發 GC 進行垃圾回收,但是如果有大量物件存活到永久代,並且仍然引用可達,那麼我們就需要堆外記憶體(Off-Heap Memory)來緩解頻繁 GC 造成的壓力。

Unsafe.allocateMemory 給了我們在直接記憶體中分配物件的能力,這塊記憶體是非堆記憶體,因此,不會受到 GC 的頻繁分析和干擾。

雖然這樣可以緩解大量物件佔用記憶體對 GC 和 JVM 造成的壓力,這也就需要我們手動管理記憶體,因此,在合適的事後我們需要手動呼叫 freeMemory 來釋放記憶體。

舉例,我們在記憶體中分配位元組陣列:

public class OffHeapArray {
    private final static int BYTE = 1;
    private long size;
    private long address;
    private Unsafe unsafe;

    public OffHeapArray(long size, Unsafe unsafe) {
        this.size = size;
        this.unsafe = unsafe;
        address = unsafe.allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        unsafe.putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return unsafe.getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }

    public void freeMemory() {
        unsafe.freeMemory(address);
    }
}
複製程式碼

我們可以通過如何程式碼分配記憶體空間:

long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE);
複製程式碼

測試用例如下:

 @Test
    public void testOffHeap() {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            offHeapArray.set((long) Integer.MAX_VALUE + i, (byte) 3);
            sum += offHeapArray.get((long) Integer.MAX_VALUE + i);
        }
        assertEquals(offHeapArray.size(), SUPER_SIZE);
        assertEquals(sum, 300);
    }
複製程式碼

一定不要忘了,在合適的時候釋放記憶體。

6. CAS(CompareAndSwap)

java.concurrent 包中提供了大量併發相關的操作,例如 AtomicInteger 就用了 Unsafe.compareAndSwap 操作來實現 lock-free 的操作,保證更好的效能。

假設我們做一個累加器,開啟 1000 個執行緒,每個執行緒迴圈累加 10_000 次。

public class CASCounter {
    private Unsafe unsafe;
    private volatile long counter = 0;
    private long offset;

    public CASCounter(Unsafe unsafe) {
        this.unsafe = unsafe;
        try {
            offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }

    public void increment() {
        long before = counter;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
            before = counter;
        }
    }


    public long getCounter() {
        return counter;
    }
}
複製程式碼

counter 需要宣告為 volatile 來保證對所有執行緒可見(參考 Java 記憶體模型以及指令集重排)

這裡的關鍵是 increment 方法,我們在 while 迴圈裡不斷嘗試呼叫 compareAndSwapLong,檢查在我們方法內部累加的同事,counter 的值有沒有被其他執行緒改變。如有沒有,就提交更改,如果不一致,那麼繼續嘗試提交更改。

測試用例程式碼如下:

 @Test
    public void testCAS() {
        int NUM_OF_THREADS = 1_000;
        int NUM_OF_INCREMENTS = 10_000;
        ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);

        IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
                .forEach(i -> service.submit(
                        () -> IntStream
                                .rangeClosed(0, NUM_OF_INCREMENTS - 1)
                                .forEach(j -> casCounter.increment())
                        )
                );
        try {
            Thread.sleep(5_000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());
    }
複製程式碼

由於累加操作需要計算時間,我們這裡暴力休眠 5s 後再驗證結果。

7. Park/Unpark

Park/Unpark 主要是被 JVM 用來做執行緒的上下文切換。當執行緒需要等待某個條件的時候,JVM 會呼叫 park 來阻塞該執行緒。

這和 Object.await 非常類似,但是 park 是作業系統呼叫,因此,在某些作業系統架構上,這會帶來更好的效能。

當執行緒阻塞後,需要再次執行, JVM 會呼叫 unpark 方法使得該執行緒變得活躍。

結論

俗話說,面試造核彈,工作擰螺絲,雖然 Unsafe 看起來不會被用到,但是能幫助我們更好的理解 JVM 以及 JDK 中 lock-free 的實現。還有一點就是 Off-Heap Memory, 如果做服務端開發中確實遇到了大記憶體物件並且常駐記憶體的情況,堆外分配不失為一個好的策略來減輕 GC 以及 GC 帶來的系統負擔(可參見 R 大在阿里 JVM 中所做的一些優化),與之對應的就是 TLAB(thread local allocation buffer),後面有機會再整理。

參考

  1. java unsafe

相關文章