可輕鬆管理大內存,JDK14外部內存訪問API探祕

隨著 JDK 14 的發佈,新版帶來了很多全新或預覽的功能,如 instanceof 模式匹配、信息量更多的 PointerExceptions、switch 表達式等。大部分功能已經被許多新聞和博客網站廣泛報道,但是孵化中的外部內存訪問 API 還沒有得到那麼多的報道,許多報道 JDK 14 的新聞都省略了它,或者只提到了 1-2 行。很可能沒有多少人知道它,也不知道它最終會允許你在 Java 中做什麼。

簡而言之,外部內存訪問 API 是 Project Panama (1) 的一部分,是對 ByteBuffer 的替代,而 ByteBuffer 之前是用於堆外內存。對於任何低級的 I/O 來說,堆外內存是需要的,因為它避免了 GC,從而比堆內內存訪問更快、更可靠。但是,ByteBuffer 也存在侷限,比如 2GB 的大小限制等。

如果你想了解更多,你可以在下面鏈接觀看 Maurizio Cimadamore 的演講 (2)。

正如上面的視頻所描述的那樣,孵化外部內存訪問 API 並不是最終的目標,而是通往更高的目標:Java 中的原生 C 庫訪問。遺憾的是,目前還沒有關於何時交付的時間表。

話雖如此,如果你想嘗試真正的好東西,那麼你可以從 Github (3) 中構建自己的 JDK。我一直在做這個工作,為我的超頻工具所需要的各種 Nvidia API 做綁定,這些 API 利用 Panama 的抽象層來使事情變得更簡單。

說了這麼多,那你實際是怎麼使用它的呢?

MemoryAddress 以及 MemorySegment

Project Panama 中的兩個主要接口是 MemoryAddress 和 MemorySegment。在外部內存訪問 API 中,獲取 MemoryAddress 首先需要使用靜態的 allocateNative 方法創建一個 MemorySegment,然後獲取該段的基本地址。

<code>

import

jdk.incubator.foreign.MemoryAddress;/<code><code>

import

jdk.incubator.foreign.MemorySegment;/<code><code>

public

class

PanamaMain

/<code><code>

{

/<code><code>

public

static

void

main

(String[] args)

/<code><code>{/<code><code> MemoryAddress address = MemorySegment.allocateNative(

4

).baseAddress;/<code><code> }/<code><code>}/<code>

當然,你可以通過 MemoryAddress 的 segment 方法再次獲取同一個 MemoryAddress 的段。在上面的例子中,我們使用的是重載的 allocateNative 方法,該方法接收了一個新的 MemorySegment 的字節大小的 long 值。這個方法還有另外兩個版本,一個是接受一個 MemoryLayout,我稍後會講到,另一個是接受一個以字節為單位的大小和字節對齊。

MemoryAddress 本身並沒有太多的API。唯一值得注意的方法是 segment 和 offset 。沒有獲取 MemoryAddress 的原始地址的方法。

而 MemorySegment 則有更多的 API。你可以通過 asByteBuffer 將 MemorySegment 轉換為 ByteBuffer,通過 close 關閉(讀:free)段(來自 AutoClosable 接口),然後用 asSlice 將其切片(後面會有更多的內容)。

好了,我們已經分配了一大塊內存,但如何對它進行讀寫呢?

MemoryHandle

MemoryHandles 是一個提供 VarHandles 的類,用於讀寫內存值。它提供了一些靜態的方法來獲取 VarHandle,但主要的方法是 varHandle,它接受下面任一類。

  • byte.class

  • short.class

  • char.class

  • int.class

  • double.class

  • long.class

(這些都不能和Object版本混淆,比如Integer.class)

在大多數情況下,你只需要通過 nativeOrder 來使用原生順序。至於你使用的類,你要使用一個適合 MemorySegment 的字節大小的類,所以在上面的例子中是 int.class,因為在 Java 中 int 佔用了 4 個字節。

一旦你創建了一個 VarHandle,你現在就可以用它來讀寫內存了。讀取是通過 VarHandle 的各種 get 方法來完成的。關於這些 get 方法的文檔並不是很有用,但簡單的說就是你把 MemoryAddress 實例傳遞給 get 方法,就像這樣。

<code>

import

java.lang.invoke.VarHandle;/<code><code>

import

java.nio.ByteOrder;/<code><code>

import

jdk.incubator.foreign.MemoryAddress;/<code><code>

import

jdk.incubator.foreign.MemoryHandles;/<code><code>

import

jdk.incubator.foreign.MemorySegment;/<code><code>

public

class

PanamaMain

/<code><code>{/<code><code>

public

static

void

main

(String[] args)

/<code><code>

{/<code><code> MemoryAddress address = MemorySegment.allocateNative(

4

).baseAddress;/<code>
<code> VarHandle handle = MemoryHandles.varHandle(

int

.

class

,

ByteOrder

.

nativeOrder

);/<code>
<code>

int

value = (

int

)handle.get(address);/<code>
<code> System.out.println(

"Memory Value: "

+ value);/<code><code> } /<code><code>}/<code>

你會注意到,這裡的 VarHandle 返回的值是類型化的。如果你以前使用過 VarHandles,這對你來說並不震驚,但如果你沒有使用過 VarHandle,那麼你只要知道這很正常,因為 VarHandle 實例返回的是 Object。

默認情況下,所有由異構內存訪問 API 分配的內存都是零。這一點很好,因為你不會在內存中留下隨機的垃圾,但對於性能關鍵的情況下可能是不好的。

至於設置一個值,你可以使用 set 方法。就像 get 方法一樣,你要傳遞地址,然後是你想傳遞到內存中的值。

<code>

import

java.lang.invoke.VarHandle;/<code><code>

import

java.nio.ByteOrder;/<code><code>

import

jdk.incubator.foreign.MemoryAddress;/<code><code>

import

jdk.incubator.foreign.MemoryHandles;/<code><code>

import

jdk.incubator.foreign.MemorySegment;/<code><code>

public

class

PanamaMain

/<code><code>{/<code><code>

public

static

void

main

(String[] args)

/<code><code>

{/<code><code> MemoryAddress address = MemorySegment.allocateNative(

4

).baseAddress;/<code>
<code> VarHandle handle = MemoryHandles.varHandle(

int

.

class

,

ByteOrder

.

nativeOrder

);/<code>
<code> handle.set(address,

10

);/<code>
<code>

int

value = (

int

)handle.get(address);/<code>
<code> System.out.println(

"Memory Value: "

+ value);/<code><code> } /<code><code>}/<code>

MemoryLayout 以及 MemoryLayouts

MemoryLayouts 類提供了 MemoryLayout 接口的預定義實現。這些接口允許你快速分配 MemorySegments,保證分配等效類型的 MemorySegments,比如 Java int。一般來說,使用這些預定義的佈局比分配大塊內存要容易得多,因為它們提供了你想要使用的常用佈局類型,而不需要查找它們的大小。

<code>

import

java.lang.invoke.VarHandle;/<code><code>

import

java.nio.ByteOrder;/<code><code>

import

jdk.incubator.foreign.MemoryAddress;/<code><code>

import

jdk.incubator.foreign.MemoryHandles;/<code><code>

import

jdk.incubator.foreign.MemoryLayouts;/<code><code>

import

jdk.incubator.foreign.MemorySegment;/<code><code>

public

class

PanamaMain

/<code><code>{/<code><code>

public

static

void

main

(String[] args)

/<code><code>

{/<code><code> MemoryAddress address = MemorySegment.allocateNative(MemoryLayouts.JAVA_INT).baseAddress;/<code>
<code> VarHandle handle = MemoryHandles.varHandle(

int

.

class

,

ByteOrder

.

nativeOrder

);/<code>
<code> handle.set(address,

10

);/<code>
<code>

int

value = (

int

)handle.get(address);/<code>
<code> System.out.println(

"Memory Value: "

+ value);/<code><code> } /<code><code>}/<code>

如果你不想使用這些預定義的佈局,你也不必這樣做。MemoryLayout(注意沒有 "s")有靜態方法,允許你創建自己的佈局。這些方法會返回一些擴展接口,例如:

  • ValueLayout

  • SequenceLayout

  • GroupLayout

ValueLayout 接口的實現是由 ofValueBits 方法返回的。它所做的就是創建一個基本的單值 MemoryLayout,就像 MemoryLayouts.JAVA_INT 一樣。

SequenceLayout 是用於創建一個像數組一樣的 MemoryLayout 的序列。接口實現是通過兩個靜態的 ofSequence 方法返回,不過只有指定長度的方法可以用來分配內存。

GroupLayout 用於結構和聯合類型的內存分配,因為它們之間相當相似。它們的接口實現來自於 structs 的 ofStruct 或 union 的 ofUnion。

如果之前沒有說清楚,MemoryLayout(s) 的使用完全是可選的,但是,它們使 API 的使用和調試變得更容易,因為你可以用常量名代替讀取原始數字。

但是,它們也有自己的問題。任何接受 var args MemoryLayout 輸入作為方法或構造函數的一部分的東西都會接受 GroupLayout 或其他 MemoryLayout,而不是預期的輸入。請確保你指定了正確的佈局。

切片和數組

MemorySegment 可以被切片,以便在一個內存塊中存儲多個值,在處理數組、結構和聯合時常用。如上文所述,這是通過 asSlice 方法來完成的。為了進行分片,你需要知道你要分片的 MemorySegment 的起始位置,單位是字節,以及存儲在該位置的值的大小,單位是字節。這將返回一個 MemorySegment,然後你可以獲得 MemoryAddress。

<code>

import

java.lang.invoke.VarHandle;/<code><code>

import

java.nio.ByteOrder;/<code><code>

import

jdk.incubator.foreign.MemoryAddress;/<code><code>

import

jdk.incubator.foreign.MemoryHandles;/<code><code>

import

jdk.incubator.foreign.MemorySegment;/<code><code>

public

class

PanamaMain

/<code><code>

{

/<code><code>

public

static

void

main

(String[] args)

/<code><code>

{/<code><code> MemoryAddress address = MemorySegment.allocateNative(

24

).baseAddress;/<code><code> MemoryAddress address1 = address.segment.asSlice(

0

,

8

).baseAddress;/<code><code> MemoryAddress address2 = address.segment.asSlice(

8

,

8

).baseAddress;/<code><code> MemoryAddress address3 = address.segment.asSlice(

16

,

8

).baseAddress;/<code><code> VarHandle handle = MemoryHandles.varHandle(

long

.class, ByteOrder.nativeOrder);/<code><code>handle.

set

(address1, Long.MIN_VALUE);/<code><code> handle.

set

(address2,

0

);/<code><code> handle.

set

(address3, Long.MAX_VALUE);/<code><code>

long

value1 = (

long

)handle.get(address1);/<code><code>

long

value2 = (

long

)handle.get(address2);/<code><code>

long

value3 = (

long

)handle.get(address3);/<code><code> System.out.println(

"Memory Value 1: "

+ value1);/<code><code> System.out.println(

"Memory Value 2: "

+ value2);/<code><code> System.out.println(

"Memory Value 3: "

+ value3);/<code><code> } /<code><code>}/<code>

這裡需要指出的是,你不需要為每個 MemoryAddress 創建新的 VarHandles。

在一個 24 字節的內存塊中,我們把它分成了 3 個不同的切片,使之成為一個數組。

你可以使用一個 for 循環來迭代它,而不是硬編碼分片值。

<code>

import

java.lang.invoke.VarHandle;/<code><code>

import

java.nio.ByteOrder;/<code><code>

import

jdk.incubator.foreign.MemoryAddress;/<code><code>

import

jdk.incubator.foreign.MemoryHandles;/<code><code>

import

jdk.incubator.foreign.MemorySegment;/<code><code>

public

class

PanamaMain

/<code><code>{/<code><code>

public

static

void

main

(String[] args)

/<code><code>

{/<code><code> MemoryAddress address = MemorySegment.allocateNative(

24

).baseAddress;/<code><code> VarHandle handle = MemoryHandles.varHandle(

long

.

class

,

ByteOrder

.

nativeOrder

);/<code><code>

for

(

int

i =

0

; i <=

2

; i++)/<code><code> {/<code><code> MemoryAddress slice = address.segment.asSlice(i*

8

,

8

).baseAddress;/<code><code> handle.set(slice, i*

8

);/<code><code> System.out.println(

"Long slice at location "

+ handle.get(slice));/<code><code> }/<code><code> } /<code><code>}/<code>

當然,你可以使用 SequenceLayout 而不是使用原始的、硬編碼的值。

<code>

import

java.lang.invoke.VarHandle;/<code><code>

import

java.nio.ByteOrder;/<code><code>

import

jdk.incubator.foreign.MemoryAddress;/<code><code>

import

jdk.incubator.foreign.MemoryHandles;/<code><code>

import

jdk.incubator.foreign.MemoryLayout;/<code><code>

import

jdk.incubator.foreign.MemoryLayouts;/<code><code>

import

jdk.incubator.foreign.MemorySegment;/<code><code>

import

jdk.incubator.foreign.SequenceLayout;/<code><code>

public

class

PanamaMain

/<code><code>{/<code><code>

public

static

void

main

(String[] args)

/<code><code>

{/<code><code> SequenceLayout layout = MemoryLayout.ofSequence(

3

, MemoryLayouts.JAVA_LONG);/<code><code> MemoryAddress address = MemorySegment.allocateNative(layout).baseAddress;/<code><code> VarHandle handle = MemoryHandles.varHandle(

long

.

class

,

ByteOrder

.

nativeOrder

);/<code><code>

for

(

int

i =

0

; i < layout.elementCount.getAsLong; i++)/<code><code> {/<code><code> MemoryAddress slice = address.segment.asSlice(i*layout.elementLayout.byteSize, layout.elementLayout.byteSize).baseAddress;/<code><code> handle.set(slice, i*layout.elementLayout.byteSize);/<code><code> System.out.println(

"Long slice at location "

+ handle.get(slice));/<code><code> }/<code><code> } /<code><code>}/<code>

不包括的內容

到目前為止,所有的東西都只在 JDK 14 的孵化版的範圍內,然而,正如前面提到的,這一切都是邁向原生 C 庫訪問的墊腳石,甚至有一兩個方法名被更改了,已經過時了。在這一切的基礎上,還有另外一層終於可以讓你訪問原生庫調用。總結一下還缺什麼。

  • jextract

  • Library 查找

  • ABI specific ValueLayout

  • Runtime ABI 佈局

  • FunctionDescriptor 接口

  • ForeignUnsafe

所有這些都是在外部訪問 API 的基礎上分層,也是對外存訪問 API 的補充。如果你打算為一些原生 C 語言庫創建綁定,那麼現在學習這些 API 就不會浪費。

文中鏈接

  1. https://openjdk.java.net/projects/panama/

  2. https://www.youtube.com/watch?v=r4dNRVWYaZI

  3. https://github.com/openjdk/panama-foreign

原文

https://medium.com/@youngty1997/jdk-14-foreign-memory-access-api-overview-70951fe221c9

高可用架構

改變互聯網的構建方式


分享到:


相關文章: