In the last blog we looked at the background to Persistent Memory, including new hardware and new processor instructions.
In this post we look at how the JDK has evolved to offer a strong platform for accessing Persistent Memory in a performant manner.
Memory-mapped I/O has long provided an efficient way to access a file as a region of memory.
In the Java platform APIs, it takes the form of MappedByteBuffer
, a class that provides a ByteBuffer
style overlay onto a range of memory that the operating system wires to a file.
The region of memory provided by the O/S when we create a mapping of a file is normally a volatile (DRAM) copy of the file contents.
To guarantee persistence, it’s necessary to call the force()
method, which the JVM implements as a msync()
system call.
msync() causes the kernel to flush the volatile in-memory copy back to the persistent storage device on which the file resides. Control returns to the Java code when the write is guaranteed to be complete, making it easy for the programmer to reason about persistence boundaries and timing in their code.
With filesystems backed by Persistent Memory, this situation can be improved. Here the underlying storage device interface is itself byte-oriented rather than block-oriented. It’s no longer necessary to make a DRAM copy of part of the persistent data in order to access it in a byte-oriented fashion.
Instead, the O/S can map memory address space directly onto the actual persistent device, bypassing the block cache. This mapping mode, known as DAX, incurs system call cost only when the mapping is created. Thereafter, system calls can be elided.
In place of expensive msync()
system calls, the application can instead use processor cache line management instructions (CLFLUSH, CLFUSHOPT and CLWB) in conjunction with memory fence operations to manage persistence boundaries.
Unfortunately, until now there was no way to do this from code running in the JVM. JNI calls could be made to functions wrapping the processor instructions, but the JVM itself was unaware of them.
Starting with JDK 14, this situation has changed. Not only can Java programmers now create MappedByteBuffers in direct (DAX) mode, they can also force()
them efficiently without resorting to JNI.
The JVM will now detect the DAX mapping and automatically optimize out the msync()
system call when force()
is invoked, replacing it with faster user space memory management instructions.
Using this feature, contributed to OpenJDK by Red Hat’s Java team, applications running on the JVM can access Persistent Memory as efficiently as platform native C/C++ applications can.
In the next blog we look at the Java platform library API changes in detail and show how to create a program that uses them to efficiently write file changes to Persistent Memory from Java.