简单易懂的现代魔法:Java sun.misc.Unsafe类探秘

《简单易懂的现代魔法》(よくわかる現代魔法)是日本小说家、原软件工程师樱坂洋撰写,宫下未纪插画的轻小说系列。小说讲述了在东京都银座,笨拙加上幼儿体形的女高中生森下历美,为了改变没有任何长处的自己,在看到魔法学校的传单后决定学习魔法(コード,Code)的故事。在魔法学校,她遇到了当今最强的魔法使……不好意思拿错剧本了。

Unsafe类简介

在之前我写的某篇讲解Spark Tungsten sort-shuffle流程的文章里曾经说过,Tungsten的内存管理是基于sun.misc.Unsafe类来做的,并且它的功能超强。为什么它要叫“不安全”这种奇怪的名字呢?Java语言的特点之一不就是“安全”么?

我们都知道,Java之所以是安全的,是因为它比C/C++这类不安全的语言额外做了很多工作,如消除指针、边界和类型检查、异常处理、GC、类加载的沙箱机制等等,可以避免用户犯的低级错误或者恶意hack代码造成灾难性后果。

Unsafe则反其道而行之,开放了很多非常低级别的操作,几乎可以自由地绕开JVM的限制“为所欲为”,因此使用它是有很大风险的,设计的本意也不是给普通用户使用的。但是,因为它特别接近底层,所以有一些框架会利用它作为优化手段,除了Spark之外,Netty、Kafka等也是如此。当然JDK内部也有不少地方应用了它,比如JUC框架,以及NIO机制等。

下面就来读读魔法吧(大雾

Unsafe类的实例化

sun包下的源码在Oracle JDK自带源码中妥妥找不到,所以我们还是得求助于OpenJDK。以OpenJDK 8u版本为例,Unsafe.java的源码参见:https://github.com/unofficial-openjdk/openjdk/blob/jdk8u/jdk8u/jdk/src/share/classes/sun/misc/Unsafe.java。

粗略扫一眼就可以知道,这里面大多数方法都是native方法。其具体实现是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()这种方式了。那么调用Unsafe.getUnsafe()方法呢?很不幸,会直接抛出SecurityException,因为它会检查调用类的ClassLoader,只有系统ClassLoader加载的类取得Unsafe实例才是安全的。

既然常规方法走不通,就只能曲线救国。下面有两种方法,萝卜白菜各有所爱,我喜欢第二种。

  • 设置bootclasspath这个JVM参数,让自己的类也通过系统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()方法允许在运行期动态创建类的定义(比如可以从已经编译好的.class文件里读取),可以创建具名类,也可以创建匿名类,这对于某些需要做代理的场合很有用。ensureClassInitialized()方法用于判断一个类是否已经初始化。

对象操作

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

allocateInstance()方法可以既不经过类的构造方法,也不经过反射而直接构造出对象实例(厉害吧!)。当需要忽略实例化之前的各种检查时,或者类没有public的构造方法时有奇效,当然它对单例类来说就是噩梦了。
staticFieldBase()与objectFieldOffset()方法则可以取得对象实例内任意一个静态字段的基地址和偏移量,配合下面的内存操作,可以修改任何已经定义好的东西,非常任性。

数组操作

public native int arrayBaseOffset(Class arrayClass);
public native int arrayIndexScale(Class arrayClass);

arrayBaseOffset()方法返回数组中第一个元素的内存偏移量,也就是整个数组的基地址。arrayIndexScale()则返回该数组中元素占用内存空间的单位大小。这两个方法配合使用,就可以通过baseOffset + i * indexScale对数组中每一个元素进行定址了。

内存操作

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()....

顾名思义,我们可以用这些方法直接干预程序中的内存管理,就像C语言中的malloc()、memset()、free()、memcpy()等函数一样。这里的内存就不单指Java堆内存了,而是本机内存,即native memory,所以有很大的自由度(当然也更容易出错)。
上面的方法包括分配、填充、复制、释放内存,以及根据内存地址进行读写的方法。可读写的内容包括所有Java的基本数据类型,还有指针(即引用地址)。

同步操作

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()....

联系JVM中monitorenter与monitorexit两条指令的用途,我们可以推断[try]monitorEnter()和monitorExit()方法是用来显式控制同步的,分别会对对象加锁和解锁。
compareAndSwapObject()方法则是CAS机制的最底层实现,它是原子性的无锁操作,效率很高,所以在JUC包中经常当做乐观锁使用。
get/putIntVolatile()等一系列方法也是根据内存地址进行读写,但保证volatile语义,即多线程环境下的可见性。

线程操作

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

park()方法会阻塞一个线程,直到调用unpark()、超时或者符合中断的条件;unpark()方法则可以唤醒阻塞的线程。它们主要出现在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的简单使用

下面是内存和数组相关的Unsafe API的用法。主要贴代码,就不多废话讲解了,毕竟都比较简单易懂(啥

实现C语言风格的sizeof()运算符

static long sizeOf(Object o) {
    Unsafe u = getUnsafe();
    HashSet fields = new HashSet();
    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实现浅拷贝

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数组大小限制

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内的使用举例

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

你可能感兴趣的:(简单易懂的现代魔法:Java sun.misc.Unsafe类探秘)