Java Magic. Part 4: sun.misc.Unsafe

来源:http://www.cnblogs.com/maxmys/archive/2016/02/06/5184012.html
-Advertisement-
Play Games

java magic翻譯系列文章,java小伙伴不知道的奇妙世界


Java Magic. Part 4: sun.misc.Unsafe

@(Base)[JDK, Unsafe, magic, 黑魔法]

轉載請寫明:原文地址

系列文章:

-Java Magic. Part 1: java.net.URL
-Java Magic. Part 2: 0xCAFEBABE
-Java Magic. Part 3: Finally
-Java Magic. Part 4: sun.misc.Unsafe

英文原文

Java是一個safe programming language,它採取了很多措施來避免programmer做傻事。例如:記憶體管理。但是在Java中也提供了一種方式,讓你可以讓你做這些傻事,使用Unsafe類。

Unsafe instantiation

在我們使用使用Unsafe之前,我們必須先獲取一個Unsafe的實例。當然我們不能直接Unsafe unsafe = new Unsafe()這樣獲取,因為Unsafe 的構造函數是私有的。但是呢有一個共有的getUnsafe()方法,但是如果你直接調用這個靜態方法的話,你可能只能收到一個SecurityException。因為這個Unsafe類只被用於授信的類。下麵我們看下這段代碼是怎麼寫的。

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

這就是java代碼如何驗證代碼是否授信。他會檢查你的代碼是由哪個classLoader載入的。

JDK的包都是由Primary ClassLoader載入的

當然我們可以讓我們的代碼也變成授信的。使用bootclasspath當啟動程式的時候,如下操作:

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

但是這尼瑪也太討厭了吧,還有別的辦法嗎?

Unsafe類有一個私有域叫做theUnsafe。我們可以通過反射來獲取這個引用。

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

這個時候一般的IDE都會提示你錯誤,"Access restriction"。我們可以配置忽略掉:

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

Unsafe API

sun.misc.Unsafe包有105個方法。但是只有下麵幾個方法可能對你來說比較重要。

  • 獲取信息類。獲取底層的記憶體信息:

    • addressSize
    • pageSize
  • 操作對象。提供了一些列操作對象和欄位的方法:

    • allocateInstance
    • objectFieldOffset
  • 操作class文件。提供了一些操作class文件的方法:

    • staticFieldOffset
    • defineClass
    • defineAnonymousClass
    • ensureClassInitialized
  • 操作數組。

    • arrayBaseOffset
    • arrayIndexScale
  • 同步機制。提供了底層的原子的同步機制

    • monitorEnter
    • tryMonitorEnter
    • monitorExit
    • compareAndSwapInt
    • putOrderedInt
  • 直接記憶體操作

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

一些有趣的例子

Avoid initialization

allocateInstance方法可以用於當你先個跳過對象的初始化方法,或者構造方法裡面的安全檢查的時候。考慮如下示例:

class A {
    private long a; // not initialized value

    public A() {
        this.a = 1; // initialization
    }

    public long a() { return this.a; }
}

我們分別通過直接調用構造函數,調用反射類庫,和unsafe來實例化:

A o1 = new A(); // constructor
o1.a(); // prints 1

A o2 = A.class.newInstance(); // reflection
o2.a(); // prints 1

A o3 = (A) unsafe.allocateInstance(A.class); // unsafe
o3.a(); // prints 0

你可以考慮下你的單例模式還好麽:)

Memory corruption

這是一個對c程式員有用的例子。另外,這個例子也是一個通用的跳過安全檢查的例子

我們看如下代碼:

class Guard {
    private int ACCESS_ALLOWED = 1;

    public boolean giveAccess() {
        return 42 == ACCESS_ALLOWED;
    }
}

Client的代碼會被安全驗證。有趣的是,這個giveAccess()函數永遠返回false,除非你有能力改變私有域ACCESS_ALLOWED。

顯然我們可以改變:

Guard guard = new Guard();
guard.giveAccess();   // false, no access

// bypass
Unsafe unsafe = getUnsafe();
Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption

guard.giveAccess(); // true, access granted

如上操作,所有的client都可以無限制的使用了。

當然上面的操作反射也可以實現。但是非常有趣的是,我們這樣操作甚至可以無需獲取對象的引用。

例如,如果我們還有一個Guard對象在這段記憶體後面。我們可以直接操作他:

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

上述代碼我們就直接操作了這個對象,註意哈,16是Guard對象在32位架構下的大小。其實我們可以直接使用sizeOf方法來獲取Guard對象的大小。好吧,我們接下來就介紹sizeOf方法

sizeOf

我們使用objectFieldOffset可以簡單實現類似於c的sizeOf函數。看如下代碼:

public static long sizeOf(Object o) {
    Unsafe u = getUnsafe();
    HashSet<Field> fields = new HashSet<Field>();
    Class c = o.getClass();
    while (c != Object.class) {
        for (Field f : c.getDeclaredFields()) {
            if ((f.getModifiers() & Modifier.STATIC) == 0) {
                fields.add(f);
            }
        }
        c = c.getSuperclass();
    }

    // get offset
    long maxSize = 0;
    for (Field f : fields) {
        long offset = u.objectFieldOffset(f);
        if (offset > maxSize) {
            maxSize = offset;
        }
    }

    return ((maxSize/8) + 1) * 8;   // padding
}

譯者註:getDeclaredFields並不能獲取父類的欄位,所以這個娃還操作了父類。

演算法思想如下:遍歷所有非靜態的欄位(包括所有父類的中),我們計算每一個欄位的大小。可能我有的地方寫錯了,但是思路也就大致如此。

當然還有一個更簡單的方法獲取size,我們可以直接讀取對象上的類結構,在JVM 1.7 32位架構上的偏移量是12。

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

下麵的normalize函數的作用是,把一個有符號int轉換為一個無符號的long。

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

有趣的是,這個方法會返回和我們上一個sizeOf函數相同的結果

實際上哈,如果你真的想使用sizeOf方法,我建議你還是使用java.lang.instrument包,但是這個需要一個JVM agent。

Shallow copy

有了計算對象大小的函數,我們同樣可以很容易搞出一個拷貝對象的函數。通常的做法是,需要你的類使用Cloneable介面。

Shallow copy:

static Object shallowCopy(Object obj) {
    long size = sizeOf(obj);
    long start = toAddress(obj);
    long address = getUnsafe().allocateMemory(size);
    getUnsafe().copyMemory(start, address, size);
    return fromAddress(address);
}

toAddressfromAddress分別從獲取某個對象的地址,和某個地址中直接讀讀取出對象。

static long toAddress(Object obj) {
    Object[] array = new Object[] {obj};
    long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
    return normalize(getUnsafe().getInt(array, baseOffset));
}

static Object fromAddress(long address) {
    Object[] array = new Object[] {null};
    long baseOffset = getUnsafe().arrayBaseOffset(Object[].class);
    getUnsafe().putLong(array, baseOffset, address);
    return array[0];
}

這個淺拷貝的函數可以拷貝任意類型。這個大小也會動態計算。但是需要你做一個簡單的類型轉換

Hide Password

還有一個有趣的Unsafe使用場景是,從記憶體中刪除那些不在需要的對象。

大部分獲取用戶密碼的API都是使用byte[]或者char[]來存儲,為什麼使用數組?

這是出於安全原因,因為我們我們可以把數組元素清空(在我們不需要他們的時候)。如果我們使用String來存儲,那麼當我們不用的時候設置這個引用為null,這時候只是簡單的清空引用,等待GC。

下麵就是個trick用來清理string對象:

String password = new String("l00k@myHor$e");
String fake = new String(password.replaceAll(".", "?"));
System.out.println(password); // l00k@myHor$e
System.out.println(fake); // ????????????

getUnsafe().copyMemory(
          fake, 0L, null, toAddress(password), sizeOf(password));

System.out.println(password); // ????????????
System.out.println(fake); // ????????????

有沒有覺得安全很多~

UPDATE: 這其實這個其實並不是真正的安全了。我們必須使用反射來清除原來String中的char數組

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

Multiple Inheritance

Java並不支持多繼承。當然我們也可以不停地做強制類型轉換。

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

上面這個小例子就是從String強制轉換成Int(如果直接強轉是有異常的)

Dynamic classes

我們可以在運行時期創建一個classes對象。如下代碼:

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

下麵是reading file:

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

這個技巧非常有用,如果你想動態創建代理或者切麵都可以。

Throw an Exception

不喜歡checkedException? 沒問題!

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

這個方法會拋出一個受檢的異常,但是你的代碼不會被強制要求必須catch。

Fast Serialization

這個例子非常有用喲。

所有人都知道,JAVA自帶的序列化方法非常的慢,而且還強制你的類有一個無參的構造函數。

Externalizable會好一點,但是可能需要你自己定義class的schema。

有一個流行的高性能序列化的庫kryo

但是以上說的,我們通通可以使用Unsafe來處理

Serialization:

  • 通過反射創建一個object的schema,這個操作每個類只需要一次。
  • 使用UnsafegetLong,getInt,getObject來獲取實際的值
  • 添加一個類的identifier
  • 把這些通通寫到文件或者別的什麼裡面去

當然最後你還可以壓縮一下來減少存儲

Deserialization:

  • 創建一個待反序列化的類,通過allocateInstance,因為這個可以不適用任何構造函數
  • 創建一個schema,和序列化裡面的操作一樣啦。
  • 從文件中獲取所有輸出
  • 使用UnsafeputLong,putInt,putObject來設置實際的值

思路大致如此,但是實際的操作中還有很多很多的細節。

不過,這麼操作起來,確實很快。可以參考kryo對Unsafe的使用,這裡

Big Arrays

大家都知道java數組的最大值就是Integer.MAX_VALUE。我們可以使用直接記憶體分配的技術來創建無限制大小的數組。

下麵是示例代碼:

class SuperArray {
    private final static int BYTE = 1;

    private long size;
    private long address;

    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }

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

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

    public long size() {
        return size;
    }
}

下麵是一個使用示例:

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

實際上,這使用了java堆外記憶體的技術,這個在java.nio包中有用到。

直接記憶體操作的技術可以讓我們在堆外分配記憶體,並且逃離GC的管理,所以你必須小心使用,並且使用Unsafe.freeMemory來釋放記憶體。這個函數也不會做任何的邊界檢查,所以很容易導致JVM崩潰。

這個技巧在數學計算上非常有用。可以存取大量的數組,這個對一些realtime的programmer非常有用,如果你無法忍受大對象的GC的話,你可以自行手動操作。

Concurrency

還有一點點內容就是Unsafe.compareAndSwap指令,所有的原子變數都是使用它來構建高性能的數據結構。

例如我們有一個簡單的Counter介面:

interface Counter {
    void increment();
    long getCounter();
}

下麵我們定義個一個Client來操作:

class CounterClient implements Runnable {
    private Counter c;
    private int num;

    public CounterClient(Counter c, int num) {
        this.c = c;
        this.num = num;
    }

    @Override
    public void run() {
        for (int i = 0; i < num; i++) {
            c.increment();
        }
    }
}

下麵是一段測試代碼:

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

第一個實現是沒有任何同步手段的Counter:

class StupidCounter implements Counter {
    private long counter = 0;

    @Override
    public void increment() {
        counter++;
    }

    @Override
    public long getCounter() {
        return counter;
    }
}

輸出是:

Counter result: 99542945
Time passed in ms: 679

運行的非常快,但是結果是錯誤的。下一個例子我們使用java內置的synchronization:

class SyncCounter implements Counter {
    private long counter = 0;

    @Override
    public synchronized void increment() {
        counter++;
    }

    @Override
    public long getCounter() {
        return counter;
    }
}

輸出:

Counter result: 100000000
Time passed in ms: 10136

結果總是正確,但是執行時間有點讓人蛋碎。下麵我們使用讀寫鎖:

lass LockCounter implements Counter {
    private long counter = 0;
    private WriteLock lock = new ReentrantReadWriteLock().writeLock();

    @Override
    public void increment() {
        lock.lock();
        counter++;
        lock.unlock();
    }

    @Override
    public long getCounter() {
        return counter;
    }
}

這讀寫鎖用法有點問題

輸出:

Counter result: 100000000
Time passed in ms: 8065

結果正確,效率高了一點。如果使用原子變數呢?

class AtomicCounter implements Counter {
    AtomicLong counter = new AtomicLong(0);

    @Override
    public void increment() {
        counter.incrementAndGet();
    }

    @Override
    public long getCounter() {
        return counter.get();
    }
}

輸出:

Counter result: 100000000
Time passed in ms: 6552

原子變數AtomicCounter效果更好一點,最後我們用Unsafe的方法來試驗一下:

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

    public CASCounter() throws Exception {
        unsafe = getUnsafe();
        offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
    }

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

    @Override
    public long getCounter() {
        return counter;
    }

輸出:

Counter result: 100000000
Time passed in ms: 6454

哦?結果和原子變數類似,是不是原子變數就是使用Unsafe來完成操作的呢?(答案是肯定的)

顯然這些sample都很簡單,但是我們也可以從中看出Unsafe的威力。

像我說過,CAS操作可以用來實現lock-free的數據結構,例如:

  • Have some state
  • Create a copy of it
  • Modify it
  • Perform CAS
  • Repeat if it fails

實際上,這些東西實現起來非常困難,遠超你的想象,而且其中有非常多的問題,例如ABA Problem, instructions reordering, 等等。

如果你真的非常感興趣,你可以看看這篇文章:Lock-Free HashMap

Bonus

Unsafepark方法,有一段非常長的英文註釋:

Block current thread, returning when a balancing unpark occurs, or a balancing unpark has already occurred, or the thread is interrupted, or, if not absolute and time is not zero, the given time nanoseconds have elapsed, or if absolute, the given deadline in milliseconds since Epoch has passed, or spuriously (i.e., returning for no "reason"). Note: This operation is in the Unsafe class only because unpark is, so it would be strange to place it elsewhere.

譯者註:park方法是用來掛起線程的,在java.concurrent.locks包下麵的AQS同步框架下應用廣泛

Conclusion

儘管Unsafe有很牛逼的用法,但是還是不推薦使用


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文原創,原文地址為:http://www.cnblogs.com/fengzheng/p/5181222.html 創建鏡像的目的 首先說DockerHub或其它一些鏡像倉庫已經提供了夠多的鏡像,有最小版本,也有一些安裝了mysql、nginx、apache等等第三方軟體的版本可以直接拿來使用。雖
  • 1.Niginx主配置文件參數詳解 a.上面博客說了在Linux中安裝nginx。博文地址為:http://www.cnblogs.com/hanyinglong/p/5102141.html b.當Nginx安裝完畢後,會有相應的安裝目錄,安裝目錄里的nginx.confg為nginx的主配置文件
  • 前段時間,項目中有個需求,需要將linux和windows的時間進行同步,網上也有很多類似時鐘同步的帖子,大致類似;不過本次的linux的機器有點特殊,沒有service命令,而且要求在另一臺suse的linux機器上通過腳本連接到目的linux機器進行時鐘同步。起先我也被困擾的很久,不過辦法都是人
  • 類的繼承,是在父類中存在可繼承的成員A,而在子類中不存在同名成員,這樣該成員會被繼承到子類,當子類對象訪問該成員時,實際訪問的是父類的對應成員。類的重寫,是在父類中存在可繼承的成員A,而在子類中存在同名成員,這樣該成員會被子類重寫,當子類對象訪問該成員時,實際訪問的是子類的成員。所以二者的區別就是,
  • 在節前的最後一天,解決了打包過程中遇到的所有問題,可以成功運行了!真是個好彩頭,希望新的一年一切順利! 以下是在使用cx_freeze過程中遇到的問題及解決辦法(Win7) 問題描述:運行exe,啟動無數個主程式,導致系統無法使用 原因:在程式中使用了multiprocessing的包 解決辦法:在
  • package CommonClassPart; import java.io.File; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public class Common
  • /*漢諾塔的玩法: * 游戲的規則:將A柱上的盤子移動到C柱上,大盤必須在小盤之上。 * 1 當A柱上只有一個盤子的時候,直接移動到C柱上; * 2 當A柱上有兩個盤子的時候, * 將A柱上的1盤(從上到下編號)移動到B柱, * 將A柱上的2盤移動到C柱, * 將B柱上的1盤移動到C柱; * (將A
  • 本文實例講述了PHP限制HTML內容中圖片必須是本站的方法。分享給大家供大家參考。具體實現方法如下: 1. PHP代碼如下: <?php $dom = new DOMDocument; $dom->loadHTML(file_get_contents('input.html')); $xpath =
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...