sun.misc.Unsafe 詳解

原文地址 譯者:許巧輝 校對:梁海艦java

Java是一門安全的編程語言,防止程序員犯不少愚蠢的錯誤,它們大部分是基於內存管理的。可是,有一種方式能夠有意的執行一些不安全、容易犯錯的操做,那就是使用Unsafe類。git

本文是sun.misc.Unsafe公共API的簡要概述,及其一些有趣的用法。
程序員

Unsafe 實例

在使用Unsafe以前,咱們須要建立Unsafe對象的實例。這並不像Unsafe unsafe = new Unsafe()這麼簡單,由於Unsafe的構造器是私有的。它也有一個靜態的getUnsafe()方法,但若是你直接調用Unsafe.getUnsafe(),你可能會獲得SecurityException異常。只能從受信任的代碼中使用這個方法。github

1 public static Unsafe getUnsafe() {
2     Class cc = sun.reflect.Reflection.getCallerClass(2);
3     if (cc.getClassLoader() != null)
4         throw new SecurityException("Unsafe");
5     return theUnsafe;
6 }

這就是Java如何驗證代碼是否可信。它只檢查咱們的代碼是否由主要的類加載器加載。算法

咱們能夠令咱們的代碼「受信任」。運行程序時,使用bootclasspath 選項,指定系統類路徑加上你使用的一個Unsafe路徑。編程

1 java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient

但這太難了。api

Unsafe類包含一個私有的、名爲theUnsafe的實例,咱們能夠經過Java反射竊取該變量。數組

1 Field f = Unsafe.class.getDeclaredField("theUnsafe");
2 f.setAccessible(true);
3 Unsafe unsafe = (Unsafe) f.get(null);

注意:忽略你的IDE。好比:eclipse顯示」Access restriction…」錯誤,但若是你運行代碼,它將正常運行。若是這個錯誤提示使人煩惱,能夠經過如下設置來避免:安全

1 Preferences -> Java -> Compiler -> Errors/Warnings ->
2 Deprecated and restricted API -> Forbidden reference -> Warning

Unsafe API

sun.misc.Unsafe類包含105個方法。實際上,對各類實體操做有幾組重要方法,其中的一些以下:數據結構

Info.僅返回一些低級的內存信息

  • addressSize
  • pageSize

Objects.提供用於操做對象及其字段的方法

  • allocateInstance
  • objectFieldOffset

Classes.提供用於操做類及其靜態字段的方法

  • staticFieldOffset
  • defineClass
  • defineAnonymousClass
  • ensureClassInitialized

Arrays.操做數組

  • arrayBaseOffset
  • arrayIndexScale

Synchronization.低級的同步原語

  • monitorEnter
  • tryMonitorEnter
  • monitorExit
  • compareAndSwapInt
  • putOrderedInt

Memory.直接內存訪問方法

  • allocateMemory
  • copyMemory
  • freeMemory
  • getAddress
  • getInt
  • putInt

有趣的用例

避免初始化

當你想要跳過對象初始化階段,或繞過構造器的安全檢查,或實例化一個沒有任何公共構造器的類,allocateInstance方法是很是有用的。考慮如下類:

1 class A {
2     private long a; // not initialized value
3  
4     public A() {
5         this.a = 1// initialization
6     }
7  
8     public long a() { return this.a; }
9 }

使用構造器、反射和unsafe初始化它,將獲得不一樣的結果。

1 A o1 = new A(); // constructor
2 o1.a(); // prints 1
3  
4 A o2 = A.class.newInstance(); // reflection
5 o2.a(); // prints 1
6  
7 A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
8 o3.a(); // prints 0

想一想全部單例發生了什麼。

內存崩潰(Memory corruption)

這對於每一個C程序員來講是常見的。順便說一下,它是繞過安全的經常使用技術。

考慮下那些用於檢查「訪問規則」的簡單類:

1 class Guard {
2        private int ACCESS_ALLOWED = 1;
3  
4        public boolean giveAccess() {
5               return 42 == ACCESS_ALLOWED;
6        }
7 }

客戶端代碼是很是安全的,而且經過調用giveAccess()來檢查訪問規則。惋惜,對於客戶,它老是返回false。只有特權用戶能夠以某種方式改變ACCESS_ALLOWED常量的值而且獲得訪問(giveAccess()方法返回true,譯者注)。

實際上,這並非真的。演示代碼以下:

1 Guard guard = new Guard();
2 guard.giveAccess();   // false, no access
3  
4 // bypass
5 Unsafe unsafe = getUnsafe();
6 Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
7 unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption
8  
9 guard.giveAccess(); // true, access granted

如今全部的客戶都擁有無限制的訪問權限。

實際上,反射能夠實現相同的功能。但值得關注的是,咱們能夠修改任何對象,甚至沒有這些對象的引用。

例如,有一個guard對象,所在內存中的位置緊接着在當前guard對象以後。咱們能夠用如下代碼來修改它的ACCESS_ALLOWED字段:

1 unsafe.putInt(guard, 16 + unsafe.objectFieldOffset(f), 42); // memory corruption

注意:咱們沒必要持有這個對象的引用。16是Guard對象在32位架構上的大小。咱們能夠手工計算它,或者經過使用sizeOf方法(它的定義,以下節)。

sizeOf

使用objectFieldOffset方法能夠實現C-風格(C-style)的sizeof方法。這個實現返回對象的自身內存大小(譯者注:shallow size)。

01 public static long sizeOf(Object o) {
02     Unsafe u = getUnsafe();
03     HashSet<Field> fields = new HashSet<Field>();
04     Class c = o.getClass();
05     while (c != Object.class) {
06         for (Field f : c.getDeclaredFields()) {
07             if ((f.getModifiers() & Modifier.STATIC) == 0) {
08                 fields.add(f);
09             }
10         }
11         c = c.getSuperclass();
12     }
13  
14     // get offset
15     long maxSize = 0;
16     for (Field f : fields) {
17         long offset = u.objectFieldOffset(f);
18         if (offset > maxSize) {
19             maxSize = offset;
20         }
21     }
22  
23     return ((maxSize/8) + 1) * 8;   // padding
24 }

算法以下:經過全部非靜態字段(包含父類的),獲取每一個字段的偏移量(offset),找到偏移最大值並填充字節數(padding)。我可能錯過一些東西,但思路是明確的。

若是咱們僅讀取對象的類結構大小值,sizeOf的實現能夠更簡單,這位於JVM 1.7 32 bit中的偏移量12。

1 public static long sizeOf(Object object){
2     return getUnsafe().getAddress(
3         normalize(getUnsafe().getInt(object, 4L)) + 12L);
4 }

normalize是一個爲了正確內存地址使用,將有符號的int類型強制轉換成無符號的long類型的方法。

1 private static long normalize(int value) {
2     if(value >= 0return value;
3     return (~0L >>> 32) & value;
4 }

真棒,這個方法返回的結果與咱們以前的sizeof方法同樣。

實際上,對於良好、安全、準確的sizeof方法,最好使用 java.lang.instrument包,但這須要在JVM中指定agent選項。

淺拷貝(Shallow copy)

爲了實現計算對象自身內存大小,咱們能夠簡單地添加拷貝對象方法。標準的解決方案是使用Cloneable修改你的代碼,或者在你的對象中實現自定義的拷貝方法,但它不會是多用途的方法。

淺拷貝:

1 static Object shallowCopy(Object obj) {
2     long size = sizeOf(obj);
3     long start = toAddress(obj);
4     long address = getUnsafe().allocateMemory(size);
5     getUnsafe().copyMemory(start, address, size);
6     return fromAddress(address);
7 }

toAddress和fromAddress將對象轉換爲其在內存中的地址,反之亦然。

01 static long toAddress(Object obj) {
02     Object[] array = new Object[] {obj};
03     long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
04     return normalize(getUnsafe().getInt(array, baseOffset));
05 }
06  
07 static Object fromAddress(long address) {
08     Object[] array = new Object[] {null};
09     long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
10     getUnsafe().putLong(array, baseOffset, address);
11     return array[0];
12 }

這個拷貝方法能夠用來拷貝任何類型的對象,動態計算它的大小。注意,在拷貝後,你須要將對象轉換成特定的類型。

隱藏密碼(Hide Password)

Unsafe中,一個更有趣的直接內存訪問的用法是,從內存中刪除沒必要要的對象。

檢索用戶密碼的大多數API的簽名爲byte[]char[],爲何是數組呢?

這徹底是出於安全的考慮,由於咱們能夠刪除不須要的數組元素。若是將用戶密碼檢索成字符串,這能夠像一個對象同樣在內存中保存,而刪除該對象只需執行解除引用的操做。可是,這個對象仍然在內存中,由GC決定的時間來執行清除。

建立具備相同大小、假的String對象,來取代在內存中原來的String對象的技巧:

01 String password = new String("l00k@myHor$e");
02 String fake = new String(password.replaceAll(".""?"));
03 System.out.println(password); // l00k@myHor$e
04 System.out.println(fake); // ????????????
05  
06 getUnsafe().copyMemory(
07           fake, 0L, null, toAddress(password), sizeOf(password));
08  
09 System.out.println(password); // ????????????
10 System.out.println(fake); // ????????????

感受很安全。

修改:這並不安全。爲了真正的安全,咱們須要經過反射刪除後臺char數組:

1 Field stringValue = String.class.getDeclaredField("value");
2 stringValue.setAccessible(true);
3 char[] mem = (char[]) stringValue.get(password);
4 for (int i=0; i < mem.length; i++) {
5   mem[i] = '?';
6 }

感謝Peter Verhas指定出這一點。

多繼承(Multiple Inheritance)

Java中沒有多繼承。

這是對的,除非咱們能夠將任意類型轉換成咱們想要的其餘類型。

1 long intClassAddress = normalize(getUnsafe().getInt(new Integer(0), 4L));
2 long strClassAddress = normalize(getUnsafe().getInt("", 4L));
3 getUnsafe().putAddress(intClassAddress + 36, strClassAddress);

這個代碼片斷將String類型添加到Integer超類中,所以咱們能夠強制轉換,且沒有運行時異常。

1 (String) (Object) (new Integer(666))

有一個問題,咱們必須預先強制轉換對象,以欺騙編譯器。

動態類(Dynamic classes)

咱們能夠在運行時建立一個類,好比從已編譯的.class文件中。將類內容讀取爲字節數組,並正確地傳遞給defineClass方法。

1 byte[] classContents = getClassContent();
2 Class c = getUnsafe().defineClass(
3               null, classContents, 0, classContents.length);
4     c.getMethod("a").invoke(c.newInstance(), null); // 1

從定義文件(class文件)中讀取(代碼)以下:

1 private static byte[] getClassContent() throws Exception {
2     File f = new File("/home/mishadoff/tmp/A.class");
3     FileInputStream input = new FileInputStream(f);
4     byte[] content = new byte[(int)f.length()];
5     input.read(content);
6     input.close();
7     return content;
8 }

當你必須動態建立類,而現有代碼中有一些代理, 這是頗有用的。

拋出異常(Throw an Exception)

不喜歡受檢異常?沒問題。

1 getUnsafe().throwException(new IOException());

該方法拋出受檢異常,但你的代碼沒必要捕捉或從新拋出它,正如運行時異常同樣。

快速序列化(Fast Serialization)

這更有實用性。

你們都知道,標準Java的Serializable的序列化能力是很是慢的。它同時要求類必須有一個公共的、無參數的構造器。

Externalizable比較好,但它須要定義類序列化的模式。

流行的高性能庫,好比kryo具備依賴性,這對於低內存要求來講是不可接受的。

unsafe類能夠很容易實現完整的序列化週期。

序列化:

  • 使用反射構建模式對象,類只可作一次。
  • 使用Unsafe方法,如getLonggetIntgetObject等來檢索實際字段值。
  • 添加類標識,以便有能力恢復該對象
  • 將它們寫入文件或任意輸出

你也能夠添加壓縮(步驟)以節省空間。

反序列化:

  • 建立已序列化對象實例,使用allocateInstance協助(便可),由於不須要任何構造器。
  • 構建模式,與序列化的步驟1相同。
  • 從文件或任意輸入中讀取全部字段。
  • 使用Unsafe方法,如putLongputIntputObject等來填充該對象。

實際上,在正確的實現過程當中還有更多的細節,但思路是明確的。

這個序列化將很是快。

順便說一下,在kryo中有使用Unsafe的一些嘗試http://code.google.com/p/kryo/issues/detail?id=75

大數組(Big Arrays

正如你所知,Java數組大小的最大值爲Integer.MAX_VALUE。使用直接內存分配,咱們建立的數組大小受限於堆大小。

SuperArray的實現

01 class SuperArray {
02     private final static int BYTE = 1;
03  
04     private long size;
05     private long address;
06  
07     public SuperArray(long size) {
08         this.size = size;
09         address = getUnsafe().allocateMemory(size * BYTE);
10     }
11  
12     public void set(long i, byte value) {
13         getUnsafe().putByte(address + i * BYTE, value);
14     }
15  
16     public int get(long idx) {
17         return getUnsafe().getByte(address + idx * BYTE);
18     }
19  
20     public long size() {
21         return size;
22     }
23 }

簡單用法:

1 long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;
2 SuperArray array = new SuperArray(SUPER_SIZE);
3 System.out.println("Array size:" + array.size()); // 4294967294
4 for (int i = 0; i < 100; i++) {
5     array.set((long)Integer.MAX_VALUE + i, (byte)3);
6     sum += array.get((long)Integer.MAX_VALUE + i);
7 }
8 System.out.println("Sum of 100 elements:" + sum);  // 300

實際上,這是堆外內存(off-heap memory)技術,在java.nio包中部分可用。

這種方式的內存分配不在堆上,且不受GC管理,因此必須當心Unsafe.freeMemory()的使用。它也不執行任何邊界檢查,因此任何非法訪問可能會致使JVM崩潰。

這可用於數學計算,代碼可操做大數組的數據。此外,這可引發實時程序員的興趣,可打破GC在大數組上延遲的限制。

併發(Concurrency)

幾句關於Unsafe的併發性。compareAndSwap方法是原子的,而且可用來實現高性能的、無鎖的數據結構。

好比,考慮問題:在使用大量線程的共享對象上增加值。

首先,咱們定義簡單的Counter接口:

1 interface Counter {
2     void increment();
3     long getCounter();
4 }

而後,咱們定義使用Counter的工做線程CounterClient

01 class CounterClient implements Runnable {
02     private Counter c;
03     private int num;
04  
05     public CounterClient(Counter c, int num) {
06         this.c = c;
07         this.num = num;
08     }
09  
10     @Override
11     public void run() {
12         for (int i = 0; i < num; i++) {
13             c.increment();
14         }
15     }
16 }

測試代碼:

01 int NUM_OF_THREADS = 1000;
02 int NUM_OF_INCREMENTS = 100000;
03 ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
04 Counter counter = ... // creating instance of specific counter
05 long before = System.currentTimeMillis();
06 for (int i = 0; i < NUM_OF_THREADS; i++) {
07     service.submit(new CounterClient(counter, NUM_OF_INCREMENTS));
08 }
09 service.shutdown();
10 service.awaitTermination(1, TimeUnit.MINUTES);
11 long after = System.currentTimeMillis();
12 System.out.println("Counter result: " + c.getCounter());
13 System.out.println("Time passed in ms:" + (after - before));

第一個無鎖版本的計數器:

01 class StupidCounter implements Counter {
02     private long counter = 0;
03  
04     @Override
05     public void increment() {
06         counter++;
07     }
08  
09     @Override
10     public long getCounter() {
11         return counter;
12     }
13 }

輸出:

1 Counter result: 99542945
2 Time passed in ms: 679

運行快,但沒有線程管理,結果是不許確的。第二次嘗試,添加上最簡單的java式同步:

01 class SyncCounter implements Counter {
02     private long counter = 0;
03  
04     @Override
05     public synchronized void increment() {
06         counter++;
07     }
08  
09     @Override
10     public long getCounter() {
11         return counter;
12     }
13 }

輸出:

1 Counter result: 100000000
2 Time passed in ms: 10136

激進的同步有效,但耗時長。試試ReentrantReadWriteLock

01 class LockCounter implements Counter {
02     private long counter = 0;
03     private WriteLock lock = new ReentrantReadWriteLock().writeLock();
04  
05     @Override
06     public void increment() {
07         lock.lock();
08         counter++;
09         lock.unlock();
10     }
11  
12     @Override
13     public long getCounter() {
14         return counter;
15     }
16 }

輸出:

1 Counter result: 100000000
2 Time passed in ms: 8065

仍然正確,耗時較短。atomics的運行效果如何?

01 class AtomicCounter implements Counter {
02     AtomicLong counter = new AtomicLong(0);
03  
04     @Override
05     public void increment() {
06         counter.incrementAndGet();
07     }
08  
09     @Override
10     public long getCounter() {
11         return counter.get();
12     }
13 }

輸出:

1 Counter result: 100000000
2 Time passed in ms: 6552

AtomicCounter的運行結果更好。最後,試試Unsafe原始的compareAndSwapLong,看看它是否真的只有特權才能使用它?

01 class CASCounter implements Counter {
02     private volatile long counter = 0;
03     private Unsafe unsafe;
04     private long offset;
05  
06     public CASCounter() throws Exception {
07         unsafe = getUnsafe();
08         offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
09     }
10  
11     @Override
12     public void increment() {
13         long before = counter;
14         while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
15             before = counter;
16         }
17     }
18  
19     @Override
20     public long getCounter() {
21         return counter;
22     }
23 }

輸出:

1 Counter result: 100000000
2 Time passed in ms: 6454

看起來彷佛等價於atomics。atomics使用Unsafe?(是的)

實際上,這個例子很簡單,但它展現了Unsafe的一些能力。

如我所說,CAS原語能夠用來實現無鎖的數據結構。背後的原理很簡單:

  • 有一些狀態
  • 建立它的副本
  • 修改它
  • 執行CAS
  • 若是失敗,重複嘗試

實際上,現實中比你現象的更難。存在着許多問題,如ABA問題、指令重排序等。

若是你真的感興趣,能夠參考lock-free HashMap的精彩展現。

修改:給counter變量添加volatile關鍵字,以免無限循環的風險。

結論(Conclusion)

即便Unsafe對應用程序頗有用,但(建議)不要使用它。

相關文章
相關標籤/搜索