Java 中提高性能的 Unsafe ,用上了沒
簡介
Unsafe 是位於 sun.misc
包下的一個類,主要提供一些用於執行低級別、不安全操作的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升 Java 運行效率、增強 Java 語言底層資源操作能力方面起到了很大的作用(比如 JUC/NIO)。
由於 Unsafe 類使 Java 語言擁有了類似 C/C++ 中的指針,來操作內存空間的能力,毫無疑問,這增加了程序發生相關指針問題的風險。
如果在程序中不當地使用 Unsafe 類會使得程序出錯的概率變大,使得 Java 這種安全的語言變得不再 “安全”,因此對 Unsafe 的使用一定要慎重。
下面我們通過例子來學學如何使用吧!
獲取 Unsafe 實例
private static sun.misc.Unsafe getUnsafe() {
try {
return AccessController.doPrivileged(new PrivilegedExceptionAction<Unsafe>() {
@Override
public sun.misc.Unsafe run() throws Exception {
Class<sun.misc.Unsafe> k = sun.misc.Unsafe.class;
for (Field f : k.getDeclaredFields()) {
f.setAccessible(true);
Object x = f.get(null);
if (k.isInstance(x)) {
return k.cast(x);
}
}
// The sun.misc.Unsafe field does not exist.
throw new Error("unsafe is null");
}
});
} catch (Throwable e) {
throw new Error("get unsafe failed", e);
}
}
Unsafe 功能列表
-
allocateMemory/freeMemory,分配、釋放堆外內存 DirectMemory(和 c/cpp 中的 malloc 一樣)
-
CAS 操作
-
copyMemory
-
defineClass(without security checks)
-
get/put address 使用堆外內存地址進行數據的讀寫操作
-
get/put volatile 使用堆外內存地址進行數據的讀寫操作 - volatile 版本
-
loadFence/storeFence/fullFence 禁止指令重排序
-
park/unpark 阻塞 / 解除阻塞線程
Unsafe 的數組操作
unsafe 中,有兩個關於數組的方法:
public native int arrayBaseOffset(Class<?> arrayClass);
public native int arrayIndexScale(Class<?> arrayClass);
base offset 含義
首先,在 Java 中,數組也是對象
In the Java programming language, arrays are objects (§4.3.1), are dynamically created, and may be assigned to variables of type Object (§4.3.2). All methods of class Object may be invoked on an array. docs.oracle.com/javase/spec…
那麼既然是對象,就會有 object header
佔用一部分空間,那麼理解數組的 base offset 也就不難了。
比如下面的這一段 JOL
輸出,實際上對象的屬性數據是從 OFFSET 12 的位置開始的,前面 0-12 的空間被 object header
所佔用。
HotSpot 64-bit VM, COOPS, 8-byte alignment
lambda.Book object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int Book.sales N/A
16 4 String Book.title N/A
20 4 LocalDate Book.publishTime N/A
24 4 String Book.author N/A
28 4 List<String> Book.tags N/A
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
那麼如果要訪問對象的屬性數據,需要基於基地址(base address)進行偏移,基地址 + 基礎偏移(base offset)+ 屬性偏移(field offset)纔是數據的內存地址(邏輯),那麼 title 屬性的內存地址實際上就是:
book(instance).title field address = book object base address + base offset + title field offset
數組在 Java 裏可以視爲一種特殊的對象,無論什麼類型的數組,他們在內存中都會有一分基礎偏移的空間,和 object header
類似。
經過測試
64 位的 JVM 數組類型的基礎偏移都是 16(測試結果在不同 JVM 下可能會有所區別)
原始類型的基礎偏移都是 12(測試結果在不同 JVM 下可能會有所區別)
可以使用 JOL 工具查看原始類型包裝類的 offset,比如 int 就看 Integer 的,雖然 jdk 沒有提供函數,但是通過 JOL 也是可以獲取的,jvm 類型和對其方式匹配即可
index scale 含義
就是指數組中每個元素所佔用的空間大小,比如 int[] scale 就是 4,long[] scale 就是 8,object[] scale 就是 4(指針大小) 有了這個 offset,就可以對數組進行 copyMemory 操作了
public native void copyMemory(
Object srcBase,
long srcOffset,
Object destBase,
long destOffset,
long bytes
);
array copy to direct memory
在使用 copyMemory 操作時,需要傳入對象及對象的 base offset,對於數組來說,offset 就是上面介紹的 offset,比如現在將一個數組中的數據拷貝至 DirectBuffer
byte[] byte = new byte[4096];
unsafe.copyMemory(byte,ARRAY_BYTE_BASE_OFFSET,null,directAddr,4096);
之所以使用 unsafe 而不是 ByteBuffer 的方法來操作 DirectBuffer,是因爲 ByteBuffer 不夠靈活。
比如我想把一個 byte[] 拷貝至 DirectBuffer 的某個位置中,就沒有相應的方法;只能先設置 position,然後再 put(byte[], int, int),非常麻煩,而且併發訪問時 position(long) 和 put 也是個非原子性操作。
但是用 unsafe 來操作的話就很輕鬆了,直接 copyMemory,直接指定 address + offset 就行。
unsafe.copyMemory(byte,ARRAY_BYTE_BASE_OFFSET,null,directAddr,4096);
copyMemory
copyMemory(
Object srcBase,
long srcOffset,
Object destBase,
long destOffset,
long bytes
)
下面來解析下該函數的參數列表:
-
srcBase 原數據對象,可以是對象、數組(對象),也可以是 Null(爲 Null 時必須指定 offset,offset 就是 address)
-
srcOffset 原數據對象的 base offset,如果 srcBase 爲空則此項爲 address
-
destBase 目標數據對象,規則同 src
-
destOffset 目標數據對象的 base offset,規則同 src
-
bytes 要拷貝的數據大小(字節單位)
通過 copyMemory 方法,可以做各種拷貝操作:對象(一般是數組)拷貝到指定堆外內存地址。
long l = unsafe.allocateMemory(1);
data2[0] = 5;
//目標是memory address,destBase爲null,destOffset爲address
unsafe.copyMemory(data2,16,null,l,1);
將對象拷貝到對象。
byte[] data1 = new byte[1];
data1[0] = 9;
byte[] data2 = new byte[1];
unsafe.copyMemory(data1,16,data2,16,1);
將堆外內存地址的數據拷貝到堆內(一般是數組)。
byte[] data2 = new byte[1];
long l = unsafe.allocateMemory(1);
unsafe.putByte(l, (byte) 2);
//源數據是memory address,srcBase爲null,srcOffset爲address
unsafe.copyMemory(null,l,data2,16,1);
堆外內存地址互相拷貝。
long l = unsafe.allocateMemory(1);
long l2 = unsafe.allocateMemory(1);
unsafe.putByte(l, (byte) 2);
//源數據是memory address,srcBase爲null,srcOffset爲address
//目標是memory address,destBase爲null,destOffset爲address
unsafe.copyMemory(null,l,null,l2,1);
Benchmark
sun.misc.Unsafe#putInt(java.lang.Object, long, int) & object field manual set
禁用 JIT 結果:
Benchmark Mode Cnt Score Error Units
ObjectFieldSetBenchmark.manualSet thrpt 2 8646455.472 ops/ns
ObjectFieldSetBenchmark.unsafeSet thrpt 2 7901066.170 ops/ns
啓用 JIT 結果:
Benchmark Mode Cnt Score Error Units
ObjectFieldSetBenchmark.manualSet thrpt 2 477232013.545 ops/ns
ObjectFieldSetBenchmark.unsafeSet thrpt 2 499135982.962 ops/ns
在本機環境下,測試的結果區別不大,也許服務器環境下應該會有更大的差距。
什麼時候該用 Unsafe
一般使用 DirectBuffer 時,需要配合 Unsafe 來使用,因爲 DirectBuffer 的內存是分配在 JVM Heap 之外的,屬於 C Heap,所以需要用直接操作內存地址(邏輯),和 C 裏 malloc 之後的操作方式一樣或者一些追求極致效率的代碼,在 JUC 或者 NIO 中,也有很多使用 Unsafe 的地方。
來源:https://juejin.cn/post/6943391357935288351
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/y0yTbqwEaYq_i3U1ICKwNw