《简单易懂的现代魔法》(よくわかる現代魔法)是日本小说家、原软件工程师樱坂洋撰写,宫下未纪插画的轻小说系列。小说讲述了在东京都银座,笨拙加上幼儿体形的女高中生森下历美,为了改变没有任何长处的自己,在看到魔法学校的传单后决定学习魔法(コード,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;
}