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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...