可轻松管理大内存,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

高可用架构

改变互联网的构建方式


分享到:


相關文章: