可輕松管理大內(nèi)存,JDK14外部內(nèi)存訪問API探秘
隨著 JDK 14 的發(fā)布,新版帶來了很多全新或預覽的功能,如 instanceof 模式匹配、信息量更多的 NullPointerExceptions、switch 表達式等。大部分功能已經(jīng)被許多新聞和博客網(wǎng)站廣泛報道,但是孵化中的外部內(nèi)存訪問 API 還沒有得到那么多的報道,許多報道 JDK 14 的新聞都省略了它,或者只提到了 1-2 行。很可能沒有多少人知道它,也不知道它最終會允許你在 Java 中做什么。
簡而言之,外部內(nèi)存訪問 API 是 Project Panama (1) 的一部分,是對 ByteBuffer 的替代,而 ByteBuffer 之前是用于堆外內(nèi)存。對于任何低級的 I/O 來說,堆外內(nèi)存是需要的,因為它避免了 GC,從而比堆內(nèi)內(nèi)存訪問更快、更可靠。但是,ByteBuffer 也存在局限,比如 2GB 的大小限制等。
如果你想了解更多,你可以在下面鏈接觀看 Maurizio Cimadamore 的演講 (2)。
正如上面的視頻所描述的那樣,孵化外部內(nèi)存訪問 API 并不是最終的目標,而是通往更高的目標:Java 中的原生 C 庫訪問。遺憾的是,目前還沒有關于何時交付的時間表。
話雖如此,如果你想嘗試真正的好東西,那么你可以從 Github (3) 中構(gòu)建自己的 JDK。我一直在做這個工作,為我的超頻工具所需要的各種 Nvidia API 做綁定,這些 API 利用 Panama 的抽象層來使事情變得更簡單。
說了這么多,那你實際是怎么使用它的呢?
MemoryAddress 以及 MemorySegment
Project Panama 中的兩個主要接口是 MemoryAddress 和 MemorySegment。在外部內(nèi)存訪問 API 中,獲取 MemoryAddress 首先需要使用靜態(tài)的 allocateNative() 方法創(chuàng)建一個 MemorySegment,然后獲取該段的基本地址。
- import jdk.incubator.foreign.MemoryAddress;
- import jdk.incubator.foreign.MemorySegment;
- public class PanamaMain
- {
- public static void main(String[] args)
- {
- MemoryAddress address = MemorySegment.allocateNative(4).baseAddress();
- }
- }
當然,你可以通過 MemoryAddress 的 segment() 方法再次獲取同一個 MemoryAddress 的段。在上面的例子中,我們使用的是重載的 allocateNative() 方法,該方法接收了一個新的 MemorySegment 的字節(jié)大小的 long 值。這個方法還有另外兩個版本,一個是接受一個 MemoryLayout,我稍后會講到,另一個是接受一個以字節(jié)為單位的大小和字節(jié)對齊。
MemoryAddress 本身并沒有太多的API。唯一值得注意的方法是 segment() 和 offset() 。沒有獲取 MemoryAddress 的原始地址的方法。
而 MemorySegment 則有更多的 API。你可以通過 asByteBuffer() 將 MemorySegment 轉(zhuǎn)換為 ByteBuffer,通過 close() 關閉(讀:free)段(來自 AutoClosable 接口),然后用 asSlice() 將其切片(后面會有更多的內(nèi)容)。
好了,我們已經(jīng)分配了一大塊內(nèi)存,但如何對它進行讀寫呢?
MemoryHandle
MemoryHandles 是一個提供 VarHandles 的類,用于讀寫內(nèi)存值。它提供了一些靜態(tài)的方法來獲取 VarHandle,但主要的方法是 varHandle,它接受下面任一類。
- byte.class
- short.class
- char.class
- int.class
- double.class
- long.class
(這些都不能和Object版本混淆,比如Integer.class)
在大多數(shù)情況下,你只需要通過 nativeOrder() 來使用原生順序。至于你使用的類,你要使用一個適合 MemorySegment 的字節(jié)大小的類,所以在上面的例子中是 int.class,因為在 Java 中 int 占用了 4 個字節(jié)。
一旦你創(chuàng)建了一個 VarHandle,你現(xiàn)在就可以用它來讀寫內(nèi)存了。讀取是通過 VarHandle 的各種 get() 方法來完成的。關于這些 get 方法的文檔并不是很有用,但簡單的說就是你把 MemoryAddress 實例傳遞給 get 方法,就像這樣。
- import java.lang.invoke.VarHandle;
- import java.nio.ByteOrder;
- import jdk.incubator.foreign.MemoryAddress;
- import jdk.incubator.foreign.MemoryHandles;
- import jdk.incubator.foreign.MemorySegment;
- public class PanamaMain
- {
- public static void main(String[] args)
- {
- MemoryAddress address = MemorySegment.allocateNative(4).baseAddress();
- VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
- int value = (int)handle.get(address);
- System.out.println("Memory Value: " + value);
- }
- }
你會注意到,這里的 VarHandle 返回的值是類型化的。如果你以前使用過 VarHandles,這對你來說并不震驚,但如果你沒有使用過 VarHandle,那么你只要知道這很正常,因為 VarHandle 實例返回的是 Object。
默認情況下,所有由異構(gòu)內(nèi)存訪問 API 分配的內(nèi)存都是零。這一點很好,因為你不會在內(nèi)存中留下隨機的垃圾,但對于性能關鍵的情況下可能是不好的。
至于設置一個值,你可以使用 set() 方法。就像 get() 方法一樣,你要傳遞地址,然后是你想傳遞到內(nèi)存中的值。
- import java.lang.invoke.VarHandle;
- import java.nio.ByteOrder;
- import jdk.incubator.foreign.MemoryAddress;
- import jdk.incubator.foreign.MemoryHandles;
- import jdk.incubator.foreign.MemorySegment;
- public class PanamaMain
- {
- public static void main(String[] args)
- {
- MemoryAddress address = MemorySegment.allocateNative(4).baseAddress();
- VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
- handle.set(address, 10);
- int value = (int)handle.get(address);
- System.out.println("Memory Value: " + value);
- }
- }
MemoryLayout 以及 MemoryLayouts
MemoryLayouts 類提供了 MemoryLayout 接口的預定義實現(xiàn)。這些接口允許你快速分配 MemorySegments,保證分配等效類型的 MemorySegments,比如 Java int。一般來說,使用這些預定義的布局比分配大塊內(nèi)存要容易得多,因為它們提供了你想要使用的常用布局類型,而不需要查找它們的大小。
- import java.lang.invoke.VarHandle;
- import java.nio.ByteOrder;
- import jdk.incubator.foreign.MemoryAddress;
- import jdk.incubator.foreign.MemoryHandles;
- import jdk.incubator.foreign.MemoryLayouts;
- import jdk.incubator.foreign.MemorySegment;
- public class PanamaMain
- {
- public static void main(String[] args)
- {
- MemoryAddress address = MemorySegment.allocateNative(MemoryLayouts.JAVA_INT).baseAddress();
- VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());
- handle.set(address, 10);
- int value = (int)handle.get(address);
- System.out.println("Memory Value: " + value);
- }
- }
如果你不想使用這些預定義的布局,你也不必這樣做。MemoryLayout(注意沒有 "s")有靜態(tài)方法,允許你創(chuàng)建自己的布局。這些方法會返回一些擴展接口,例如:
- ValueLayout
- SequenceLayout
- GroupLayout
ValueLayout 接口的實現(xiàn)是由 ofValueBits() 方法返回的。它所做的就是創(chuàng)建一個基本的單值 MemoryLayout,就像 MemoryLayouts.JAVA_INT 一樣。
SequenceLayout 是用于創(chuàng)建一個像數(shù)組一樣的 MemoryLayout 的序列。接口實現(xiàn)是通過兩個靜態(tài)的 ofSequence() 方法返回,不過只有指定長度的方法可以用來分配內(nèi)存。
GroupLayout 用于結(jié)構(gòu)和聯(lián)合類型的內(nèi)存分配,因為它們之間相當相似。它們的接口實現(xiàn)來自于 structs 的 ofStruct() 或 union 的 ofUnion()。
如果之前沒有說清楚,MemoryLayout(s) 的使用完全是可選的,但是,它們使 API 的使用和調(diào)試變得更容易,因為你可以用常量名代替讀取原始數(shù)字。
但是,它們也有自己的問題。任何接受 var args MemoryLayout 輸入作為方法或構(gòu)造函數(shù)的一部分的東西都會接受 GroupLayout 或其他 MemoryLayout,而不是預期的輸入。請確保你指定了正確的布局。
切片和數(shù)組
MemorySegment 可以被切片,以便在一個內(nèi)存塊中存儲多個值,在處理數(shù)組、結(jié)構(gòu)和聯(lián)合時常用。如上文所述,這是通過 asSlice() 方法來完成的。為了進行分片,你需要知道你要分片的 MemorySegment 的起始位置,單位是字節(jié),以及存儲在該位置的值的大小,單位是字節(jié)。這將返回一個 MemorySegment,然后你可以獲得 MemoryAddress。
- import java.lang.invoke.VarHandle;
- import java.nio.ByteOrder;
- import jdk.incubator.foreign.MemoryAddress;
- import jdk.incubator.foreign.MemoryHandles;
- import jdk.incubator.foreign.MemorySegment;
- public class PanamaMain
- {
- public static void main(String[] args)
- {
- MemoryAddress address = MemorySegment.allocateNative(24).baseAddress();
- MemoryAddress address1 = address.segment().asSlice(0, 8).baseAddress();
- MemoryAddress address2 = address.segment().asSlice(8, 8).baseAddress();
- MemoryAddress address3 = address.segment().asSlice(16, 8).baseAddress();
- VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());
- handle.set(address1, Long.MIN_VALUE);
- handle.set(address2, 0);
- handle.set(address3, Long.MAX_VALUE);
- long value1 = (long)handle.get(address1);
- long value2 = (long)handle.get(address2);
- long value3 = (long)handle.get(address3);
- System.out.println("Memory Value 1: " + value1);
- System.out.println("Memory Value 2: " + value2);
- System.out.println("Memory Value 3: " + value3);
- }
- }
這里需要指出的是,你不需要為每個 MemoryAddress 創(chuàng)建新的 VarHandles。
在一個 24 字節(jié)的內(nèi)存塊中,我們把它分成了 3 個不同的切片,使之成為一個數(shù)組。
你可以使用一個 for 循環(huán)來迭代它,而不是硬編碼分片值。
- import java.lang.invoke.VarHandle;
- import java.nio.ByteOrder;
- import jdk.incubator.foreign.MemoryAddress;
- import jdk.incubator.foreign.MemoryHandles;
- import jdk.incubator.foreign.MemorySegment;
- public class PanamaMain
- {
- public static void main(String[] args)
- {
- MemoryAddress address = MemorySegment.allocateNative(24).baseAddress();
- VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());
- for(int i = 0; i <= 2; i++)
- {
- MemoryAddress slice = address.segment().asSlice(i*8, 8).baseAddress();
- handle.set(slice, i*8);
- System.out.println("Long slice at location " + handle.get(slice));
- }
- }
- }
當然,你可以使用 SequenceLayout 而不是使用原始的、硬編碼的值。
- import java.lang.invoke.VarHandle;
- import java.nio.ByteOrder;
- import jdk.incubator.foreign.MemoryAddress;
- import jdk.incubator.foreign.MemoryHandles;
- import jdk.incubator.foreign.MemoryLayout;
- import jdk.incubator.foreign.MemoryLayouts;
- import jdk.incubator.foreign.MemorySegment;
- import jdk.incubator.foreign.SequenceLayout;
- public class PanamaMain
- {
- public static void main(String[] args)
- {
- SequenceLayout layout = MemoryLayout.ofSequence(3, MemoryLayouts.JAVA_LONG);
- MemoryAddress address = MemorySegment.allocateNative(layout).baseAddress();
- VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder());
- for(int i = 0; i < layout.elementCount().getAsLong(); i++)
- {
- MemoryAddress slice = address.segment().asSlice(i*layout.elementLayout().byteSize(), layout.elementLayout().byteSize()).baseAddress();
- handle.set(slice, i*layout.elementLayout().byteSize());
- System.out.println("Long slice at location " + handle.get(slice));
- }
- }
- }
不包括的內(nèi)容
到目前為止,所有的東西都只在 JDK 14 的孵化版的范圍內(nèi),然而,正如前面提到的,這一切都是邁向原生 C 庫訪問的墊腳石,甚至有一兩個方法名被更改了,已經(jīng)過時了。在這一切的基礎上,還有另外一層終于可以讓你訪問原生庫調(diào)用??偨Y(jié)一下還缺什么。
- jextract
- Library 查找
- ABI specific ValueLayout
- Runtime ABI 布局
- FunctionDescriptor 接口
- ForeignUnsafe
所有這些都是在外部訪問 API 的基礎上分層,也是對外存訪問 API 的補充。如果你打算為一些原生 C 語言庫創(chuàng)建綁定,那么現(xiàn)在學習這些 API 就不會浪費。
文中鏈接
- https://openjdk.java.net/projects/panama/
- https://www.youtube.com/watch?v=r4dNRVWYaZI
- https://github.com/openjdk/panama-foreign
原文
https://medium.com/@youngty1997/jdk-14-foreign-memory-access-api-overview-70951fe221c9
本文轉(zhuǎn)載自微信公眾號「高可用架構(gòu)」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系高可用架構(gòu)公眾號。