簡單易懂的現(xiàn)代魔法:Java sun.misc.Unsafe類探秘

《簡單易懂的現(xiàn)代魔法》(よくわかる現(xiàn)代魔法)是日本小說家、原軟件工程師櫻坂洋撰寫,宮下未紀插畫的輕小說系列缭保。小說講述了在東京都銀座,笨拙加上幼兒體形的女高中生森下歷美蝙茶,為了改變沒有任何長處的自己艺骂,在看到魔法學(xué)校的傳單后決定學(xué)習(xí)魔法(コード,Code)的故事隆夯。在魔法學(xué)校钳恕,她遇到了當今最強的魔法使……不好意思拿錯劇本了。

Unsafe類簡介

在之前我寫的某篇講解Spark Tungsten sort-shuffle流程的文章里曾經(jīng)說過蹄衷,Tungsten的內(nèi)存管理是基于sun.misc.Unsafe類來做的忧额,并且它的功能超強。為什么它要叫“不安全”這種奇怪的名字呢愧口?Java語言的特點之一不就是“安全”么睦番?

我們都知道,Java之所以是安全的耍属,是因為它比C/C++這類不安全的語言額外做了很多工作托嚣,如消除指針、邊界和類型檢查恬涧、異常處理、GC碴巾、類加載的沙箱機制等等溯捆,可以避免用戶犯的低級錯誤或者惡意hack代碼造成災(zāi)難性后果。

Unsafe則反其道而行之厦瓢,開放了很多非常低級別的操作提揍,幾乎可以自由地繞開JVM的限制“為所欲為”,因此使用它是有很大風(fēng)險的煮仇,設(shè)計的本意也不是給普通用戶使用的劳跃。但是,因為它特別接近底層浙垫,所以有一些框架會利用它作為優(yōu)化手段刨仑,除了Spark之外,Netty夹姥、Kafka等也是如此杉武。當然JDK內(nèi)部也有不少地方應(yīng)用了它,比如JUC框架辙售,以及NIO機制等轻抱。

下面就來讀讀魔法吧(大霧

Unsafe類的實例化

sun包下的源碼在Oracle JDK自帶源碼中妥妥找不到,所以我們還是得求助于OpenJDK旦部。以O(shè)penJDK 8u版本為例祈搜,Unsafe.java的源碼參見:https://github.com/unofficial-openjdk/openjdk/blob/jdk8u/jdk8u/jdk/src/share/classes/sun/misc/Unsafe.java较店。

粗略掃一眼就可以知道,這里面大多數(shù)方法都是native方法容燕。其具體實現(xiàn)是C++代碼梁呈,在GCC的libjava中,傳送門:https://github.com/gcc-mirror/gcc/blob/gcc-4_7-branch/libjava/sun/misc/natUnsafe.cc缰趋。

先來觀察一下Unsafe如何實例化捧杉。

    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }

Unsafe是一個典型的單例,所以不要指望new Unsafe()這種方式了秘血。那么調(diào)用Unsafe.getUnsafe()方法呢味抖?很不幸,會直接拋出SecurityException灰粮,因為它會檢查調(diào)用類的ClassLoader仔涩,只有系統(tǒng)ClassLoader加載的類取得Unsafe實例才是安全的。

既然常規(guī)方法走不通粘舟,就只能曲線救國熔脂。下面有兩種方法,蘿卜白菜各有所愛柑肴,我喜歡第二種霞揉。

  • 設(shè)置bootclasspath這個JVM參數(shù),讓自己的類也通過系統(tǒng)ClassLoader加載:
java -Xbootclasspath:/usr/jdk1.8.0/jre/lib/rt.jar:. path.to.your.own.UnsafeClass
  • 不用Unsafe.getUnsafe()晰骑,而用反射直接取得Unsafe類中的theUnsafe私有字段:
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(null);

Unsafe APIs

Unsafe類提供了多達一百個方法适秩,但從功能的角度分類,就十分清晰了硕舆,下面簡單列舉一些秽荞。

類操作

public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
public native void ensureClassInitialized(Class<?> c);

defineClass()與defineAnonymousClass()方法允許在運行期動態(tài)創(chuàng)建類的定義(比如可以從已經(jīng)編譯好的.class文件里讀取)抚官,可以創(chuàng)建具名類扬跋,也可以創(chuàng)建匿名類,這對于某些需要做代理的場合很有用凌节。ensureClassInitialized()方法用于判斷一個類是否已經(jīng)初始化钦听。

對象操作

public native Object allocateInstance(Class<?> cls) throws InstantiationException;
public native Object staticFieldBase(Field f)
public native long objectFieldOffset(Field f);

allocateInstance()方法可以既不經(jīng)過類的構(gòu)造方法,也不經(jīng)過反射而直接構(gòu)造出對象實例(厲害吧1渡荨)彪见。當需要忽略實例化之前的各種檢查時,或者類沒有public的構(gòu)造方法時有奇效娱挨,當然它對單例類來說就是噩夢了余指。
staticFieldBase()與objectFieldOffset()方法則可以取得對象實例內(nèi)任意一個靜態(tài)字段的基地址和偏移量,配合下面的內(nèi)存操作,可以修改任何已經(jīng)定義好的東西酵镜,非常任性碉碉。

數(shù)組操作

public native int arrayBaseOffset(Class<?> arrayClass);
public native int arrayIndexScale(Class<?> arrayClass);

arrayBaseOffset()方法返回數(shù)組中第一個元素的內(nèi)存偏移量,也就是整個數(shù)組的基地址淮韭。arrayIndexScale()則返回該數(shù)組中元素占用內(nèi)存空間的單位大小垢粮。這兩個方法配合使用,就可以通過baseOffset + i * indexScale對數(shù)組中每一個元素進行定址了靠粪。

內(nèi)存操作

public native long allocateMemory(long bytes);
public native void setMemory(Object o, long offset, long bytes, byte value);
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
public native void freeMemory(long address);
public native long getAddress(long address);
public native void putAddress(long address, long x);
public native int getInt(long address);
public native void putInt(long address, int x);
// getByte()/putByte(), getDouble()/setDouble()....

顧名思義蜡吧,我們可以用這些方法直接干預(yù)程序中的內(nèi)存管理,就像C語言中的malloc()占键、memset()昔善、free()、memcpy()等函數(shù)一樣畔乙。這里的內(nèi)存就不單指Java堆內(nèi)存了君仆,而是本機內(nèi)存,即native memory牲距,所以有很大的自由度(當然也更容易出錯)返咱。
上面的方法包括分配、填充牍鞠、復(fù)制咖摹、釋放內(nèi)存,以及根據(jù)內(nèi)存地址進行讀寫的方法难述∮┣纾可讀寫的內(nèi)容包括所有Java的基本數(shù)據(jù)類型,還有指針(即引用地址)龄广。

同步操作

public native void monitorEnter(Object o);
public native void monitorExit(Object o);
public native boolean tryMonitorEnter(Object o);
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public native int getIntVolatile(Object o, long offset);
public native void putIntVolatile(Object o, long offset, int x);
// getLongVolatile()/setLongVolatile()....

聯(lián)系JVM中monitorenter與monitorexit兩條指令的用途硫眯,我們可以推斷[try]monitorEnter()和monitorExit()方法是用來顯式控制同步的蕴侧,分別會對對象加鎖和解鎖择同。
compareAndSwapObject()方法則是CAS機制的最底層實現(xiàn),它是原子性的無鎖操作净宵,效率很高敲才,所以在JUC包中經(jīng)常當做樂觀鎖使用。
get/putIntVolatile()等一系列方法也是根據(jù)內(nèi)存地址進行讀寫择葡,但保證volatile語義紧武,即多線程環(huán)境下的可見性。

線程操作

public native void park(boolean isAbsolute, long time);
public native void unpark(Object thread);

park()方法會阻塞一個線程敏储,直到調(diào)用unpark()阻星、超時或者符合中斷的條件;unpark()方法則可以喚醒阻塞的線程。它們主要出現(xiàn)在j.u.c.locks.LockSupport類中妥箕。

存取柵欄操作

public native void loadFence();
public native void storeFence();
public native void fullFence();

這三個方法是在JDK8才加入的滥酥,用來防止存取操作時的指令重排序。loadsFence()禁止在柵欄前重排序load指令畦幢,storeFence()禁止在柵欄前重排序store指令坎吻,fullFence()則是兩者兼而有之。

Unsafe API的簡單使用

下面是內(nèi)存和數(shù)組相關(guān)的Unsafe API的用法宇葱。主要貼代碼瘦真,就不多廢話講解了,畢竟都比較簡單易懂(啥

實現(xiàn)C語言風(fēng)格的sizeof()運算符

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();
    }
    long maxSize = 0;
    for (Field f : fields) {
        long offset = u.objectFieldOffset(f);
        if (offset > maxSize) {
            maxSize = offset;
        }
    }
    return ((maxSize / 8) + 1) * 8;  
}

不用Cloneable實現(xiàn)淺拷貝

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);
}

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];
}

突破Java數(shù)組大小限制

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;
    }
}

Unsafe API在JDK內(nèi)的使用舉例

java.util.concurrent.atomic.AtomicInteger

    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }

    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    public final boolean weakCompareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }

    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }

    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }

java.util.concurrent.locks.LockSupport

    private static void setBlocker(Thread t, Object arg) {
        UNSAFE.putObject(t, parkBlockerOffset, arg);
    }

    public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

    public static void parkNanos(Object blocker, long nanos) {
        if (nanos > 0) {
            Thread t = Thread.currentThread();
            setBlocker(t, blocker);
            UNSAFE.park(false, nanos);
            setBlocker(t, null);
        }
    }

    public static void parkUntil(Object blocker, long deadline) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(true, deadline);
        setBlocker(t, null);
    }

    public static Object getBlocker(Thread t) {
        if (t == null)
            throw new NullPointerException();
        return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
    }

    public static void park() {
        UNSAFE.park(false, 0L);
    }

    public static void parkNanos(long nanos) {
        if (nanos > 0)
            UNSAFE.park(false, nanos);
    }

    public static void parkUntil(long deadline) {
        UNSAFE.park(true, deadline);
    }

java.nio.DirectByteBuffer

    DirectByteBuffer(int cap) {
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);
        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

    public ByteBuffer put(byte x) {
        unsafe.putByte(ix(nextPutIndex()), ((x)));
        return this;
    }

    public ByteBuffer put(int i, byte x) {
        unsafe.putByte(ix(checkIndex(i)), ((x)));
        return this;
    }

    public ByteBuffer put(ByteBuffer src) {
        if (src instanceof DirectByteBuffer) {
            if (src == this)
                throw new IllegalArgumentException();
            DirectByteBuffer sb = (DirectByteBuffer)src;
            int spos = sb.position();
            int slim = sb.limit();
            assert (spos <= slim);
            int srem = (spos <= slim ? slim - spos : 0);
            int pos = position();
            int lim = limit();
            assert (pos <= lim);
            int rem = (pos <= lim ? lim - pos : 0);
            if (srem > rem)
                throw new BufferOverflowException();
            unsafe.copyMemory(sb.ix(spos), ix(pos), (long)srem << 0);
            sb.position(spos + srem);
            position(pos + srem);
        } else if (src.hb != null) {
            int spos = src.position();
            int slim = src.limit();
            assert (spos <= slim);
            int srem = (spos <= slim ? slim - spos : 0);
            put(src.hb, src.offset + spos, srem);
            src.position(spos + srem);
        } else {
            super.put(src);
        }
        return this;
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末黍瞧,一起剝皮案震驚了整個濱河市诸尽,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌雷逆,老刑警劉巖弦讽,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異膀哲,居然都是意外死亡往产,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門某宪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仿村,“玉大人,你說我怎么就攤上這事兴喂“遥” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵衣迷,是天一觀的道長畏鼓。 經(jīng)常有香客問我,道長壶谒,這世上最難降的妖魔是什么云矫? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮汗菜,結(jié)果婚禮上让禀,老公的妹妹穿的比我還像新娘。我一直安慰自己陨界,他們只是感情好巡揍,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著菌瘪,像睡著了一般腮敌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天糜工,我揣著相機與錄音斗这,去河邊找鬼。 笑死啤斗,一個胖子當著我的面吹牛表箭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播钮莲,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼免钻,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了崔拥?” 一聲冷哼從身側(cè)響起极舔,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎链瓦,沒想到半個月后拆魏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡慈俯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年渤刃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贴膘。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡卖子,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出刑峡,到底是詐尸還是另有隱情洋闽,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布突梦,位于F島的核電站诫舅,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏宫患。R本人自食惡果不足惜刊懈,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望撮奏。 院中可真熱鬧俏讹,春花似錦当宴、人聲如沸畜吊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玲献。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捌年,已是汗流浹背瓢娜。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留礼预,地道東北人眠砾。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像托酸,于是被迫代替她去往敵國和親褒颈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349