转载

可轻松管理大内存,JDK14外部内存访问API探秘

随着 JDK 14 的发布,新版带来了很多全新或预览的功能,如 instanceof 模式匹配、信息量更多的 NullPointerExceptions、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,然后获取该段的基本地址。

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 的字节大小的 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 方法,就像这样。

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。

默认情况下,所有由异构内存访问 API 分配的内存都是零。这一点很好,因为你不会在内存中留下随机的垃圾,但对于性能关键的情况下可能是不好的。

至于设置一个值,你可以使用 set() 方法。就像 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());


handle.set(address, 10);


int value = (int)handle.get(address);


System.out.println("Memory Value: " + value);

}

}

MemoryLayout 以及 MemoryLayouts

MemoryLayouts 类提供了 MemoryLayout 接口的预定义实现。这些接口允许你快速分配 MemorySegments,保证分配等效类型的 MemorySegments,比如 Java int。一般来说,使用这些预定义的布局比分配大块内存要容易得多,因为它们提供了你想要使用的常用布局类型,而不需要查找它们的大小。

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")有静态方法,允许你创建自己的布局。这些方法会返回一些扩展接口,例如:

  • 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。

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 创建新的 VarHandles。

在一个 24 字节的内存块中,我们把它分成了 3 个不同的切片,使之成为一个数组。

你可以使用一个 for 循环来迭代它,而不是硬编码分片值。

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));

}

}

}

不包括的内容

到目前为止,所有的东西都只在 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

参考阅读:

  • JDK 14发布,空指针错误改进正式落地

  • 新项目用 Rust 还是 Go ?

  • 如何快速定位 Redis 热 key?

  • dubbo-go 1.4.0 版本发布,支持 K8s 注册中心、rest 协议

  • 从lstio的角度谈微服务的一些误区

本文由高可用架构翻译。技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。

高可用架构

改变互联网的构建方式

可轻松管理大内存,JDK14外部内存访问API探秘 长按二维码 关注「高可用架构」公众号

原文  http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=2653552044&idx=1&sn=1bcc7a7cbfe8c15a5e8b5cc3a5c287d9
正文到此结束
Loading...