13

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

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ%3D%3D&%3Bmid=2653552044&%3Bidx=1&%3Bsn=1bcc7a7cbfe8c15a5e8b5cc3a5c287d9
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

随着 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

参考阅读:

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

高可用架构

改变互联网的构建方式

vEjQNvu.jpg!web 长按二维码 关注「高可用架构」公众号


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK