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

存储 存储软件
外部内存访问 API 是 Project Panama (1) 的一部分,是对 ByteBuffer 的替代,而 ByteBuffer 之前是用于堆外内存。对于任何低级的 I/O 来说,堆外内存是需要的,因为它避免了 GC,从而比堆内内存访问更快、更可靠。

[[321267]]

 随着 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,然后获取该段的基本地址。

 

  1. import jdk.incubator.foreign.MemoryAddress; 
  2. import jdk.incubator.foreign.MemorySegment; 
  3. public class PanamaMain 
  4.     public static void main(String[] args) 
  5.         MemoryAddress address = MemorySegment.allocateNative(4).baseAddress(); 
  6.     } 

当然,你可以通过 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 方法,就像这样。

 

  1. import java.lang.invoke.VarHandle; 
  2. import java.nio.ByteOrder; 
  3. import jdk.incubator.foreign.MemoryAddress; 
  4. import jdk.incubator.foreign.MemoryHandles; 
  5. import jdk.incubator.foreign.MemorySegment; 
  6. public class PanamaMain 
  7.     public static void main(String[] args) 
  8.     { 
  9.         MemoryAddress address = MemorySegment.allocateNative(4).baseAddress(); 
  10.  
  11.         VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder()); 
  12.  
  13.         int value = (int)handle.get(address); 
  14.  
  15.         System.out.println("Memory Value: " + value); 
  16.     }  

你会注意到,这里的 VarHandle 返回的值是类型化的。如果你以前使用过 VarHandles,这对你来说并不震惊,但如果你没有使用过 VarHandle,那么你只要知道这很正常,因为 VarHandle 实例返回的是 Object。

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

至于设置一个值,你可以使用 set() 方法。就像 get() 方法一样,你要传递地址,然后是你想传递到内存中的值。

 

  1. import java.lang.invoke.VarHandle; 
  2. import java.nio.ByteOrder; 
  3. import jdk.incubator.foreign.MemoryAddress; 
  4. import jdk.incubator.foreign.MemoryHandles; 
  5. import jdk.incubator.foreign.MemorySegment; 
  6. public class PanamaMain 
  7.     public static void main(String[] args) 
  8.     { 
  9.         MemoryAddress address = MemorySegment.allocateNative(4).baseAddress(); 
  10.  
  11.         VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder()); 
  12.  
  13.         handle.set(address, 10); 
  14.  
  15.         int value = (int)handle.get(address); 
  16.  
  17.         System.out.println("Memory Value: " + value); 
  18.     }   

MemoryLayout 以及 MemoryLayouts

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

 

  1. import java.lang.invoke.VarHandle; 
  2. import java.nio.ByteOrder; 
  3. import jdk.incubator.foreign.MemoryAddress; 
  4. import jdk.incubator.foreign.MemoryHandles; 
  5. import jdk.incubator.foreign.MemoryLayouts; 
  6. import jdk.incubator.foreign.MemorySegment; 
  7. public class PanamaMain 
  8.     public static void main(String[] args) 
  9.     { 
  10.         MemoryAddress address = MemorySegment.allocateNative(MemoryLayouts.JAVA_INT).baseAddress(); 
  11.  
  12.         VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder()); 
  13.  
  14.         handle.set(address, 10); 
  15.  
  16.         int value = (int)handle.get(address); 
  17.  
  18.         System.out.println("Memory Value: " + value); 
  19.     }   

如果你不想使用这些预定义的布局,你也不必这样做。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。

 

  1. import java.lang.invoke.VarHandle; 
  2. import java.nio.ByteOrder; 
  3. import jdk.incubator.foreign.MemoryAddress; 
  4. import jdk.incubator.foreign.MemoryHandles; 
  5. import jdk.incubator.foreign.MemorySegment; 
  6. public class PanamaMain 
  7.     public static void main(String[] args) 
  8.     { 
  9.         MemoryAddress address = MemorySegment.allocateNative(24).baseAddress(); 
  10.          
  11.         MemoryAddress address1 = address.segment().asSlice(0, 8).baseAddress(); 
  12.         MemoryAddress address2 = address.segment().asSlice(8, 8).baseAddress(); 
  13.         MemoryAddress address3 = address.segment().asSlice(16, 8).baseAddress(); 
  14.          
  15.         VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder()); 
  16. handle.set(address1, Long.MIN_VALUE); 
  17.         handle.set(address2, 0); 
  18.         handle.set(address3, Long.MAX_VALUE); 
  19.          
  20.         long value1 = (long)handle.get(address1); 
  21.         long value2 = (long)handle.get(address2); 
  22.         long value3 = (long)handle.get(address3); 
  23.          
  24.          
  25.         System.out.println("Memory Value 1: " + value1); 
  26.         System.out.println("Memory Value 2: " + value2); 
  27.         System.out.println("Memory Value 3: " + value3); 
  28.     }   

这里需要指出的是,你不需要为每个 MemoryAddress 创建新的 VarHandles。

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

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

 

  1. import java.lang.invoke.VarHandle; 
  2. import java.nio.ByteOrder; 
  3. import jdk.incubator.foreign.MemoryAddress; 
  4. import jdk.incubator.foreign.MemoryHandles; 
  5. import jdk.incubator.foreign.MemorySegment; 
  6. public class PanamaMain 
  7.     public static void main(String[] args) 
  8.     { 
  9.         MemoryAddress address = MemorySegment.allocateNative(24).baseAddress(); 
  10.          
  11.         VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder()); 
  12.          
  13.         for(int i = 0; i <= 2; i++) 
  14.         { 
  15.             MemoryAddress slice = address.segment().asSlice(i*8, 8).baseAddress(); 
  16.              
  17.             handle.set(slice, i*8); 
  18.              
  19.             System.out.println("Long slice at location " + handle.get(slice)); 
  20.         } 
  21.     }   

当然,你可以使用 SequenceLayout 而不是使用原始的、硬编码的值。

 

  1. import java.lang.invoke.VarHandle; 
  2. import java.nio.ByteOrder; 
  3. import jdk.incubator.foreign.MemoryAddress; 
  4. import jdk.incubator.foreign.MemoryHandles; 
  5. import jdk.incubator.foreign.MemoryLayout; 
  6. import jdk.incubator.foreign.MemoryLayouts; 
  7. import jdk.incubator.foreign.MemorySegment; 
  8. import jdk.incubator.foreign.SequenceLayout; 
  9. public class PanamaMain 
  10.     public static void main(String[] args) 
  11.     { 
  12.         SequenceLayout layout = MemoryLayout.ofSequence(3, MemoryLayouts.JAVA_LONG); 
  13.         MemoryAddress address = MemorySegment.allocateNative(layout).baseAddress(); 
  14.          
  15.         VarHandle handle = MemoryHandles.varHandle(long.class, ByteOrder.nativeOrder()); 
  16.          
  17.         for(int i = 0; i < layout.elementCount().getAsLong(); i++) 
  18.         { 
  19.             MemoryAddress slice = address.segment().asSlice(i*layout.elementLayout().byteSize(), layout.elementLayout().byteSize()).baseAddress(); 
  20.              
  21.             handle.set(slice, i*layout.elementLayout().byteSize()); 
  22.              
  23.             System.out.println("Long slice at location " + handle.get(slice)); 
  24.         } 
  25.     }   

不包括的内容

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

本文转载自微信公众号「高可用架构」,可以通过以下二维码关注。转载本文请联系高可用架构公众号。

 

 

 

责任编辑:武晓燕 来源: 高可用架构
相关推荐

2021-09-05 06:00:47

电脑内存Windows

2010-09-25 09:56:46

JVM最大内存

2021-04-30 20:20:36

HugePages大内存页系统

2024-04-08 07:27:02

JDK8ZGC垃圾回收

2010-04-23 11:18:05

Ubuntu 10.0

2018-07-23 09:26:08

iOS内存优化

2013-10-11 17:32:18

Linux运维内存管理

2011-03-17 16:51:51

SQLServer数据加速剂

2023-10-18 13:31:00

Linux内存

2018-10-19 09:51:05

服务器内存RAM

2010-09-17 14:04:14

JVM内存设置

2021-11-05 16:33:32

手机内存技术

2013-10-12 13:01:51

Linux运维内存管理

2018-12-06 12:58:50

CPU内存模块

2019-07-10 05:08:05

CPU内存分页管理

2018-03-16 14:04:45

Linux大内存页虚拟内存管理

2021-04-30 19:53:53

HugePages大内存页物理

2009-02-09 08:55:12

ArcGIS API发布Silverlight

2011-07-20 17:04:43

Objective-C 内存 内存泄露

2014-07-28 15:01:56

Android内存
点赞
收藏

51CTO技术栈公众号