文件: | 下载 |
许可: | 3-Clause BSD 许可 |
面向...优化 | |
---|---|
操作系统: | Linux* 内核版本 4.3 或更高版本 |
硬件: | 模拟:参考如何使用动态随机访问内存 (DRAM) 模拟永久性内存 |
软件: (编程语言、工具、IDE、框架) | C++ 编译器、JDK、永久性内存开发人员套件 (PMDK) 库和面向 Java* (PCJ) 的永久性集合 |
前提条件: | 熟悉 C++ 和 Java |
简介
在本文中,我将介绍面向永久性内存编程的 Java* (PCJ) API 的永久性集合。该 API 面向永久性集合,因为集合类别可有效映射至许多永久性内存应用的用例。我演示了如何对永久性集合进行实例化和存储(不进行序列化),并稍后在重启后提取它。本文详细描述了一个完整示例(包括源代码),其包含一个员工永久性数组(从头开始实施的员工自定义永久性类别)。在本文最后,我说明了如何编译和运行使用 PCJ 的 Java 程序。
我们为何需要 API?
NVM 编程模型 (NPM) 标准由存储和网络行业协会 (SNIA) 中的业内主要厂商制定,内存映射文件是其中的核心。选择该模型的主要目的是避免无谓的重复工作,他们努力解决的多数问题(例如,如何采集和查找内存选项,及对其命名,或如何提供访问控制、权限等)已被文件系统 (FS) 解决。此外,内存映射文件已存在数十年。因此,它们非常稳定、易于掌握且得到广泛支持。使用专门 FS,用户空间中运行的流程可在打开和映射文件后直接访问映射内存,无需 FS 的直接支持,从而避免了高成本的数据块高速缓存/刷新,及操作系统 (OS) 双向上下文切换。
然而,直接对照内存映射文件进行编程并非无关紧要。即使我们可避免动态随机访问内存 (DRAM) 上的数据块高速缓存,最新写入的一些数据可能仍会驻留在 CPU 高速缓存中(不会刷新)。遗憾的是,这些高速缓存未受到突然断电方面的保护。如果发生这种情况,而且高速缓存中的部分写入内容仍未刷新,数据结构可能会损坏。为避免这一问题,程序员在设计数据结构时需允许临时残缺写入 (torn-write),确保及时发布合适的刷新指令(刷新过多会影响性能,因此也不可取)。
可喜的是,英特尔开发了 永久性内存开发人员套件 (PMDK),其包括一系列开源库和工具,可提供低级基元和实用的高级抽象化功能,从而帮助永久性内存程序员克服这些障碍。尽管这些库采用 C 实施,企业也在努力提供适用于其他主流语言的 API,包括 C++、Java*(本文主题)及 Python*。尽管面向 Java 和 Python 的 API 仍处于初期的试验性阶段,但相关工作正在稳步、快速推进。
面向 Java* 的永久性集合 (PCJ)
该 API 支持在 Java 中进行永久性内存编程,主要面向永久性集合,原因在于集合类别可有效映射至许多永久性内存应用的用例。相比于 Java 虚拟机 (JVM) 实例,这些类别的实例可持续(可访问)更长时间。除内置类别外,程序员还可定义自己的永久性类别(见下文)。我们甚至可通过低级存储器 API 创建自己的抽象化功能(采用 MemoryRegion
接口形式),但该话题不在本文的探讨范围。
下面列出了该 API 支持的永久性集合:
- 永久性基本数组:
PersistentBooleanArray、PersistentByteArray、PersistentCharArray、PersistentDoubleArray、PersistentFloatArray、PersistentIntArray、PersistentLongArray、PersistentShortArray
- 永久性数组:
PersistentArray<AnyPersistent>
- 永久性元组:
PersistentTuple<AnyPersistent, …>
- 永久性元组列表:
PersistentArrayList<AnyPersistent>
- 永久性哈希图:
PersistentHashMap<AnyPersistent, AnyPersistent>
- 永久性链接列表:
PersistentLinkedList<AnyPersistent>
- 永久性链接队列:
PersistentLinkedQueue<AnyPersistent>
- 永久性跳跃列表图:
PersistentSkipListMap<AnyPersistent, AnyPersistent>
- 永久性 FP 树图:
PersistentFPTreeMap<AnyPersistent, AnyPersistent>
- 永久性 SI 哈希图:
PersistentSIHashMap<AnyPersistent, AnyPersistent>
类似于 PMDK 中的 C/C++ libpmemobj库,我们需要通用的根对象固定在永久性内存池中创建的所有其他对象。对于 PCJ,该操作可通过名为 ObjectDirectory
的单例类别完成。在内部,该类别可使用 PersistentHashMap<PersistentString, AnyPersistent>
类哈希图对象实施,这意味着我们可使用人类可读的名称存储和提取对象,如以下代码片段所示:
... PersistentIntArray data = new PersistentIntArray(1024); ObjectDirectory.put("My_fancy_persistent_array", data); // no serialization data.set(0, 123); ...
该代码首先分配大小为 1024 的永久性整数数组。此外,它会将其参考插入名为 "My_fancy_persistent_array"
的 ObjectDirectory
。最后,该代码将一个整数写入该数组的第一个位置。因此,如果我们并未将参考插入对象目录,并丢失了对象的最后参考(例如,由于实施 data = null
),Java 垃圾回收器 (GC) 会采集对象,并将其内存区域从永久性池中释放(这意味着该对象会永久丢失)。这种情况不会发生在 C/C++ libpmemobj库中;在相似的情况下会发生永久性内存泄露(不过泄露对象可以恢复)。
以下代码片段显示我们可在重启后提取对象:
... PersistentIntArray old_data = ObjectDirectory.get("My_fancy_persistent_array", PersistentIntArray.class); assert(old_data.get(0) == 123); ...
您可以看到,我们无需对新数组实施实例化。变量old_data
直接分配至永久性内存中存储的名为 "My_fancy_persistent_array"
的对象。assert()
在此处用于确保数组相同。
完整示例
现在,让我们看一个完整示例,了解各个部分如何有序组合(您可下载源代码 from GitHub*)。
import lib.util.persistent.*; @SuppressWarnings("unchecked") public class EmployeeList { static PersistentArray<Employee> employees; public static void main(String[] args) { // fetching back main employee list (or creating it if it is not there) if (ObjectDirectory.get("employees", PersistentArray.class) == null) { employees = new PersistentArray<Employee>(64); ObjectDirectory.put("employees", employees); System.out.println("Storing objects"); // creating objects for (int i = 0; i < 64; i++) { Employee employee = new Employee(i, new PersistentString("Fake Name"), new PersistentString("Fake Department")); employees.set(i, employee); } } else { // reading objects for (int i = 0; i < 64; i++) { assert(employees.get(i).getId() == i); } } } }
上述代码列表对应着类别EmployeeList
(定义请见EmployeeList.java
文件),其包含程序的 main()
方法。该方法会尝试获取永久性数组“员工”的参考。如果参考不存在(即返回值为零),大小为 64 的全新PersistentArray
对象将被创建,参考会保存在ObjectDirectory
中。完成该操作后,数组将包含 64 个员工对象。如果该数组存在,我们会对其重复实施,以确保员工 ID 的数值即为我们之前插入的数值。
有关该代码的一些详情需在此说明。首先,需导入lib.util.persistent.*
下的软件包所包含的所需类别。除永久性集合外,PersistentString
等永久性内存的基础类别也包含在其中。如果用过 C/C++ 接口,您可能希望了解我们会把池文件的位置及其大小等信息传送至库的什么位置。对于 PCJ,需使用名为 config.properties
(需驻留在当前的工作目录上)的配置文件实施该操作。以下示例将池路径设置为/mnt/mem/persistent_heap
,并将其大小设置为 2GB(假设永久性内存设备—真实设备或使用 RAM 模拟—安装在 /mnt/mem
) 中:
$ cat config.properties path=/mnt/mem/persistent_heap size=2147483648 $
如上所述,如果简单的类型(如整数、字符串等)无法满足需求,需要更复杂的类型进行补充,我们可定义自己的永久性类别。本示例中员工
就是这样的类别。该类别如下表所示(您可在文件 Employee.java
中找到它):
import lib.util.persistent.*; import lib.util.persistent.types.*; public final class Employee extends PersistentObject { private static final LongField ID = new LongField(); private static final StringField NAME = new StringField(); private static final StringField DEPARTMENT = new StringField(); private static final ObjectType<Employee> TYPE = ObjectType.withFields(Employee.class, ID, NAME, DEPARTMENT); public Employee (long id, PersistentString name, PersistentString department) { super(TYPE); setLongField(ID, id); setName(name); setDepartment(department); } private Employee (ObjectPointer<Employee> p) { super(p); } public long getId() { return getLongField(ID); } public PersistentString getName() { return getObjectField(NAME); } public PersistentString getDepartment() { return getObjectField(DEPARTMENT); } public void setName(PersistentString name) { setObjectField(NAME, name); } public void setDepartment(PersistentString department) { setObjectField(DEPARTMENT, department); } public int hashCode() { return Long.hashCode(getId()); } public boolean equals(Object obj) { if (!(obj instanceof Employee)) return false; Employee emp = (Employee)obj; return emp.getId() == getId() && emp.getName().equals(getName()); } public String toString() { return String.format("Employee(%d, %s)", getId(), getName()); } }
乍一看到这个代码,您可能发现它与 Java 中定义的任何普通类别相似。首先,我们有定义为 私有
的类别字段(以及 静态常量
,更多信息请见下)。包括两个构造器。第一个构造器利用id
、名称
和部门
等参数构建一个新的永久性对象,需要将其类型定义(ObjectType<Employee>
的实例)传送至父类别 PersistentObject
(所有自定义类别需要将该类别作为其继承路径中的祖先)。第二个构建器需要通过作为参数传送的另一个员工对象 (p
) 进行自我复制,从而构建一个新的永久性对象。在这种情况下,整个对象 p 都可传送至父类别。 最后,我们可获得 getter 和 setter,以及所有其他公共方法。
您还可能注意到字段声明的方式有点奇怪。我们为何没有将 ID 声明为普通的 long?
,或将其命名为string?
?此外,字段为何被声明为静态常量
?原因在于它们并非传统字段,而是 元字段。它们只用作 PersistentObject
的引导,支持访问永久性内存中的实际偏置字段。将他们声明为静态常量
可帮助我们为相同类别的所有对象提供一个元字段副本。
元字段只是未获得 Java 原生支持的永久性对象的伪影。PCJ 使用元字段将永久性对象分布在永久性堆上,并依靠 PMDK 库实施内存分配和事务支持。PMDK 库的原生代码需使用Java 原生接口 (JNI)调用。如需查看该实施堆栈的高级概览,请参见图 1。
图 1.面向 Java* (PCJ) 实施堆栈的永久性集合的高级概览。
下面,我将谈论一下事务。借助 PCJ,可通过所提供的存储器方法(如 setLongField()
或 setObjectField()
)对永久性字段自动实施任何修改。这意味着,如果在字段写入过程中发生电源故障,修改可以恢复(从而避免数据损坏,如长字符串上的残缺写入)。然而,如果要一次为多个字段实施原子性,需要执行明确事务。本文不会详细说明这些事务。如果希望了解事务在 C/C++ 中的处理方式,您可阅读下面的 C 中 pmemobj 事务简介以及C++ 中 pmemobj 事务简介。
下面的片段显示了 PCJ 事务的基本特征:
... Transaction.run(()->{ // transactional code }); ...
如何运行
如果您从 GitHub中下载了该示例,所提供的 Makefile 可通过各自存储库下载 PCJ 和 PMDK。您只需要系统上安装的 C++ 编译器(当然还有 Java)。然而,我将在这里向您介绍编译和运行永久性内存 Java 程序所需的步骤。如需实施这些操作,您需在系统上安装 PMDK 和 PCJ。
编译 Java 类别时,您需要指定 PCJ 类别路径。如果您在主目录上安装了 PCJ,请执行以下操作:
$ javac -cp .:/home/<username>/pcj/target/classes Employee.java $ javac -cp .:/home/<username>/pcj/target/classes EmployeeList.java $
然后,您会看到生成的*.class
文件。为了在 EmployeeList.class
内运行 main()
方法,您需要(再次)传送 PCJ 类别路径。您还需要将java.library.path
环境变量设置为用作 PCJ 和 PMDK 间桥接器的已编译原生库的位置:
$ java -cp .:/…/pcj/target/classes -Djava.library.path=/…/pcj/target/cppbuild EmployeeList
总结
在本文中,我介绍了面向永久性内存编程的 Java API。该 API 面向永久性集合,因为集合类别可有效映射至许多永久性内存应用的用例。我演示了如何对永久性集合进行实例化和存储(不进行序列化),并稍后在重启后提取它。本文详细描述了一个完整示例,其包含一个员工永久性数组(从头开始实施的员工自定义永久性类别)。在本文最后,我说明了如何编译和运行使用 PCJ 的 Java 程序。
关于作者
Eduardo Berrocal 于 2017 年 7 月加入英特尔,担任云软件工程师。此前,他在伊利诺斯州芝加哥市的伊利诺理工大学(IIT)获得了计算机科学博士学位。他的博士研究方向主要为(但不限于)数据分析和面向高性能计算的容错。他曾担任过贝尔实验室(诺基亚)的暑期实习生、阿贡国家实验室的研究助理,芝加哥大学的科学程序员和 web 开发人员以及西班牙 CESVIMA 实验室的实习生。