技术与生活 性能之重 持久内存编程指南—乱译连载 (23.附录D.Java for 持久内存)

持久内存编程指南—乱译连载 (23.附录D.Java for 持久内存)

在编写本书时,Java中持久内存的native支持工作正在努力进行中。当前的特性大多是不稳定的,这意味着一旦应用程序退出,数据就不会持久化。我们已经描述了几个已经集成的特性,并展示了两个库(LLPL和PCJ),它们为Java应用程序提供了额外的功能。

23  附录D.Java for 持久内存

Java是最流行的编程语言之一,因为它快速、安全、可靠。有很多应用程序和web站点是用Java实现的。它是跨平台的并且支持多CPU架构,支持从笔记本电脑到数据中心、从游戏机到科学超级计算机、从手机到互联网、从CD/DVD播放器到汽车。Java无处不在!

在写这本书的时候,Java并不支持在持久性内存上持久地存储数据,并且没有持久性内存开发工具包(PMDK)的Java绑定,所以我们认为Java不值得专门写一章。考虑到Java在开发人员中的受欢迎程度,我们不想将其排除在本书之外,所以我们决定在本附录中包含有关Java的信息。

在本附录中,我们将描述已经集成到Oracle的Java开发工具包(JDK)中的特性[https://www.oracle.com/java/]和OpenJDK[https://openjdk.java.net/]. 我们还提供了关于Java中提议的持久内存功能的信息,以及开发中的两个外部Java库。

23.1  持久内存作为易失性内存使用

对于具有异构内存体系结构的系统上的易失性用例,Java确实支持持久内存。这是一个具有DRAM、持久内存和非易失性存储(如SSD或NVMe驱动器)的系统。

23.1.1 可换内存设备上的堆分配

Oracle JDK v10和openjdkv10都实现了JEP 316:在可换内存设备上的堆分配[http://openjdk.java.net/jeps/316]. 此功能的目标是使HotSpot VM能够在用户指定的可换内存设备(如持久内存)上分配Java对象堆。

如第3章所述,Linux和Windows可以通过文件系统公开持久内存。例如NTFS和XFS或ext4。这些直接访问(DAX)文件系统上的内存映射文件绕过页面缓存,提供虚拟内存到设备上物理内存的直接映射。

为了使用DAX文件系统上的内存映射文件分配Java堆,Java添加了一个新的运行时选项,-XX:AllocateHeapAt=<path>。此选项采用DAX文件系统的路径,并使用内存映射在内存设备上分配对象堆。使用此选项,HotSpot VM可以在用户指定的可换内存设备(如持久内存)上分配Java对象堆。该特性的目的不是打算在多个运行的JVM之间共享一个非易失性区域,也不打算重用同一个区域来进一步调用JVM。

图D-1显示了使用DRAM和持久内存支持的虚拟内存的新堆分配方法的体系结构。

图23-1 Java堆内存:从易失性内存分配和使用the “-XX:AllocatedHeapAt=” 选项从持久内存分配

Java堆只从持久内存分配。非堆内存组件,如:代码缓存、gc记帐等则是从DRAM分配的。

现有的堆相关标志(如-Xmx、-Xms和垃圾收集相关标志)将一如既往地继续工作。例如:

$ java –Xmx32g –Xms16g –XX:AllocateHeapAt=/pmemfs/jvmheap \ ApplicationClass

这将分配初始16GiB堆大小(-Xms),最大堆大小可达32GiB(-Xmx32g)。JVM堆可以使用在–XX:AllocateHeapAt=/pmemfs/JVM heap指定的路径中创建的临时文件的容量。JVM自动创建临时文件jvmheap.XXXXXX,其中,XXXXXX是随机生成的数字。目录路径应该是一个持久的内存支持的文件系统,使用DAX选项加载。有关使用DAX功能装入文件系统的更多信息,请参阅第3章。

为了确保应用程序安全,实现必须确保在文件系统中创建的文件是:

  • 受正确权限保护,防止其他用户访问;
  • 在任何可能的情况下,当应用程序终止时删除。

临时文件是用运行JVM的用户的读写权限创建的,JVM在终止之前删除该文件。

此功能以具有与DRAM相同语义(包括原子操作的语义)的替代内存设备为目标,因此可以在不更改现有应用程序代码的情况下用于对象堆而不是DRAM。所有其他内存结构(如代码堆、元空间、线程堆栈等)将继续驻留在DRAM中。

此功能的一些用例包括:

  • 在多JVM部署中,一些JVM(如守护进程、服务等)的优先级比其他JVM低。与DRAM相比,持久内存可能具有更高的访问延迟。低优先级进程可以为堆使用持久内存,从而允许高优先级进程使用更多的DRAM;
  • 大数据和内存数据库等应用程序对内存的需求日益增长。这样的应用程序可以为堆使用持久内存,因为持久内存模块可能具有比DRAM更大的容量;
  • 有关此功能的详细信息,请参阅以下资源:
    • Oracle JavaSE 10文档[https://docs.oracle.com/ javase/10/tools/java.htm#GUID-3B1CE181-CD30-4178-9602230B800D4FAE__BABCBGHF];
    • OpenJDK JEP 316:替代内存设备上的堆分配[http://openjdk.java.net/jeps/316]。

23.1.2 替代内存设备上的部分堆分配

热点JVM 12.0.1引入了一个特性,可以在用户指定的替代内存设备(如持久内存)上分配老年代Java堆。

G1和并行GC中的特性允许它们在持久内存中分配一部分堆内存,专门用于老年代对象。堆的其余部分映射到DRAM,年轻一代的对象总是放在这里。

操作系统通过文件系统公开持久性内存设备,因此可以直接访问底层媒体或直接访问(DAX)。支持DAX的文件系统包括Microsoft Windows上的NTFS和Linux上的ext4和XFS。这些文件系统中的内存映射文件绕过文件缓存,提供虚拟内存到设备上物理内存的直接映射。DAX挂载文件系统的路径指定使用标志-XX:AllocateOldGenAt=<path>启用此功能。没有其他标志可启用此功能。

启用时,年轻代对象仅放置在DRAM中,而老年代对象始终分配在持久内存中。在任何给定点上,垃圾收集器都保证DRAM和持久内存中提交的总内存始终小于-Xmx指定的堆大小。

启用时,JVM还根据可用DRAM限制年轻一代的最大大小,尽管建议用户显式设置年轻一代的最大大小。

例如,如果在具有32GB DRAM和1024GB持久内存的系统上使用-Xmx756g执行JVM,垃圾收集器将基于以下规则限制年轻一代的大小:

  • 未指定-XX:MaxNewSize或-Xmn:最大年轻代大小设置为可用内存的80%(25.6GB);
  • -XX:MaxNewSize或-Xmn已指定:无论指定的大小如何,最大年轻代大小都限制在可用内存(25.6GB)的80%;
  • 用户可以使用-XX: MaxRAM让VM知道可以使用多少DRAM。如果指定,则最大年轻代大小设置为MaxRAM中值的80%;
  • 用户可以为年轻一代指定要使用的DRAM百分比,而不是默认的80%;
  • -XX:最大百分比;
  • 使用日志选项gc+ergo=info启用日志将在启动时打印最大年轻代大小。

23.1.3 非易失性映射字节缓冲区

JEP 352:非易失性映射字节缓冲区[https://openjdk.java.net/jeps/352]添加新的规范于JDK的文件映射模式,以便可以使用FileChannel API创建引用持久内存的MappedByteBuffer实例。这个特性应该在Java 14中发布时才可用,这是在本书出版之后。

这个JEP建议升级MappedByteBuffer以支持对持久内存的访问。唯一需要的API更改是FileChannel客户机使用新枚举来请求DAX文件系统(而不是传统的文件存储系统)上文件的映射。最近对MappedByteBufer API的更改意味着它支持允许直接内存更新所需的所有行为,并提供更高级别Java客户端库实现持久数据类型(如块文件系统、日志记录、持久对象等)所需的持久性保证。FileChannel和MappedByteBuffer的实现需要修改以了解映射文件的这种新的备份类型。

JEP的主要目标是确保客户机能够高效、一致地访问和更新Java程序中的持久内存。这个目标的一个关键要素是确保对缓冲区区域的独立写操作(或小组连续写操作)以最小的开销提交,也就是说,确保可能仍在缓存中的任何更改都被写回内存。

第二个从属目标是使用在类unsafe中定义的受限制的JDK内部API实现此提交行为,从而允许MappedByteBuffer以外的需要提交到持久内存类重用它。

最后一个相关的目标是允许映射到持久内存上的缓冲区被现有的监视和管理APIs跟踪。

已经可以将持久内存设备文件映射到MappedByteBuffer,并使用当前的force() 方法提交写操作,例如,使用Intel的libpmem库作为设备驱动程序,或调用libpmem作为本机库。然而,使用当前的API,这两种实现都提供了一个“sledgehammer”解决方案。Force不能区分干净的行和脏的行,并且需要系统调用或JNI调用来实现每个写回。由于这两个原因,现有的能力无法满足该JEP的效率要求。

这个JEP的目标OS/CPU平台组合是Linux/x64和Linux/AArch64。实行这一限制有两个原因。此功能仅适用于支持mmap系统调用MAP_SYNC标志的操作系统,该标志允许对非易失性内存进行同步映射。最近的Linux版本也是如此。它也只能在用户空间控制下支持缓存线写回的CPU上工作。x64和AArch64都提供了指令满足此需求。

23.2  Java持久集合 (PCJ)

Persistent Collections for Java library(PCJ)是Intel为持久内存编程开发的一个开源Java库。有关PCJ的更多信息,包括源代码和示例代码,请访问GitHub https://github.com/pmem/pcj。

在写这本书的时候,PCJ库仍然被定义为一个“pilot”项目,并且仍然处于实验状态。现在可以使用它,希望它在探索现有Java代码的改进以使用持久内存以及探索一般的持久Java编程方面是有用的。

库提供了一系列线程安全的持久集合类,包括数组、列表和映射。它还提供对字符串、原始整数和浮点类型的持久支持。开发人员也可以定义自己的持久类。

这些持久类的实例的行为与常规Java对象非常相似,但它们的字段存储在持久内存中。与常规Java对象一样,它们的生命周期是基于可访问性的;如果没有对它们的显式引用,它们将被自动垃圾收集。与常规Java对象不同,它们的生命周期可以超出Java虚拟机的单个实例,也可以超越机器重新启动。

因为持久对象的内容是保留的,所以即使在发生崩溃和电源故障时,保持对象的数据一致性也是很重要的。库中的持久集合和其他对象在Java方法级别提供持久数据一致性。方法(包括字段设置器)的行为就像该方法对持久内存的更改全部发生或没有发生一样。使用PCJ提供的事务API,开发人员定义的类也可以实现这种方法级的一致性。

PCJ使用持久内存开发工具包(PMDK)中的libpmemobj库,我们在第7章中讨论了这个库。有关PMDK的更多信息,请访问https://pmem.io/以及https://github.com/pmem/pmdk

23.2.1 Java应用中使用PCJ

要将此库导入现有的Java应用程序,请在Java类路径中包含工程的target/classes目录,在java.library.path路径中包含工程的target/cppbuild. 例如:

$ javac -cp .:<path>/pcj/target/classes <source>
$ java -cp .:<path>/pcj/target/classes \
    -Djava.library.path=<path>/pcj/target/cppbuild <class>

使用PCJ库有几种方法:

  1. 在应用程序中使用内置持久类的实例;
  2. 用新方法扩展内置的持久类;
  3. 声明新的持久类或扩展内置类的方法和持久字段。

PCJ源代码示例可以在下面列出的参考资料中找到:

  1. Java持久集合简介- https://github. com/pmem/pcj/blob/master/Introduction.txt;
  2. 代码示例:Java*持久内存编程API简介- https://software.intel.com/en-us/articles/code-sample-introduction-to-java-api-for-persistentmemory-programming
  3. 代码示例:使用Java*持久集合(PCJ)创建一个“Hello World”程序- https://software.intel.com/en-us/articles/code-sample-introduction-to-java-api-for-persistentmemory-programming

23.3  低层持久库 (LLPL)

低层持久性库(LLPL)是Intel为持久性内存编程开发的一个开源Java库。通过提供对内存块级别的持久内存的java访问,LLPL为开发人员建立自定义抽象或修改现有代码提供了基础。有关LLPL的更多信息,包括源代码、示例代码和javadocs,可以在GitHub的https:// github.com/pmem/llpl。

该库提供持久内存堆的管理,以及堆中持久内存块的手动分配和释放。Java持久内存块类提供了在块内读写Java整数类型的方法,以及在块之间、块与(易失性)Java字节数组之间复制字节的方法。

有几种不同的堆和相应的内存块可用于帮助实现不同的数据一致性方案。此类可实施方案的示例:

  • 事务性:内存中的数据在崩溃或断电后可用;
  • 持久性:在受控进程退出后,内存中的数据可用;
  • 易失性:大容量持久性存储器,退出后数据不再需要。

混合数据一致性方案也可以实现。例如,对关键数据进行事务性写入,对不太关键的数据(如统计数据或缓存)进行持久性或易失性写入。

LLPL使用持久内存开发工具包(PMDK)中的libpmemobj库,该库我们在第7章讨论过。有关PMDK的更多信息,请访问https://pmem.io/以及https://github.com/pmem/pmdk

23.3.1 在Java应用程序中使用LLPL

要在Java应用程序中使用LLPL,需要在系统上安装PMDK和LLPL。要编译Java类,需要指定LLPL类路径。假设在home目录中安装了LLPL,请执行以下操作:

$ javac -cp .:/home/<username>/llpl/target/classes LlplTest.java

执行后,将看到生成的∗.class文件。要在类中运行main() 方法,需要再次传递LLPL类路径。你还需要设置java.library.path路径用作LLPL和PMDK之间桥梁的编译native库位置的环境变量:

$ java -cp .:/.../llpl/target/classes \
 -Djava.library.path=/.../llpl/target/cppbuild LlplTest

PCJ源代码示例可以在下面列出的参考资料中找到:

  • 代码示例:介绍Java*的低层持久库(LLPL)https://software.intel.com/en-us/articles/introducing-the-low-level-persistent-library-llpl-for-java
  • 代码示例:使用Java*的低层持久性库(LLPL)创建一个“Hello World”程序–https://software.intel.com/en-us/articles/code-sample-create-a-hello-worldprogram-using-the-low-level-persistence-library-llpl-for-java;
  • 在Java中实现持久内存使用–https://www.snia.org/sites/default/files/PM-Summit/2019/presentations/05PMSummit19-Dohrmann.pdf。

23.4  本章小结

在编写本书时,Java中持久内存的native支持工作正在努力进行中。当前的特性大多是不稳定的,这意味着一旦应用程序退出,数据就不会持久化。我们已经描述了几个已经集成的特性,并展示了两个库(LLPL和PCJ),它们为Java应用程序提供了额外的功能。

低层持久库(LLPL)是Intel为持久内存编程开发的一个开源Java库。通过提供对内存块级别的持久内存的java访问,LLPL为开发人员建立自定义抽象或修改现有代码提供了基础。

更高级别的Java持久性集合(PCJ)为开发人员提供了一系列线程安全的持久性集合类,包括数组、列表和映射。它还提供对字符串、原始整数和浮点类型的持久支持。开发人员也可以定义自己的持久类。

作者: charlie_chen

编程是一生最爱: >> 架构与设计; >> 软件工程; >> 项目管理; >> 产品研发。
联系我们

联系我们

022-XXXXXXXX

在线咨询: QQ交谈

邮箱: 1549889473@qq.com

欢迎交流。
关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部