《簡單易懂的現(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;
}