JEP 370: Foreign-Memory Access API
February 18, 2020

JEP 370: Foreign-Memory Access API for JDK 14

Java Updates
Java Application Development

With the new JDK release cadence, it seems like there’s a new JDK enhancement proposal (JEP) every week. The JEP we’re looking at today is JEP 370: Foreign-Memory Access API (Incubator), a feature for JDK 14. In this article, we will discuss how the Foreign-Memory Access API works — including an exploration of the three main abstractions of the API; MemorySegment, MemoryAddress, and MemoryLayout.

Back to top

JEP 370: Foreign-Memory Access API (Incubator)

The release notes summarize JEP 370 as introducing an API that allows Java programs to access foreign memory outside the Java heap.

The reasoning for introducing this API, as shown in the JEP notes, is two-fold:

  1. Share memory across multiple processes.
  2. Serialize/deserialize memory content by mapping file into memory.

Albeit a motivation for the JEP, the current implementation of JEP 370 doesn't provide a way to easily share memory between processes.

Does JEP 370: Foreign Memory Access Add New Functionality?

In short: no. Many Java libraries are already able to access foreign memory, so enabling the JDK to access foreign memory outside the Java heap doesn’t add functionality to what’s already out there.

But what the Foreign-Memory Access API brings is a more supported, and potentially more interoperable, way to handle foreign memory within the JVM without relying on external libraries.

When considering the current APIs in the JDK used in achieving the same task — the ByteBuffer API, introduced in 2002 with Java 1.4; JNI, which requires a per-architecture native implementation; and the unsupported and aptly named Unsafe API, which allows access to any memory location — the Foreign-Memory Access API is long overdue.

Find the Latest Java News

For more information on the latest Java releases and features, visit our Java release hub.

Explore Java Resources 

Back to top

JEP 370 and Project Panama

JEP 370 is part of Project Panama and marks a continued attempt to make the JVM and non-Java APIs more interoperable. Adding a reliable and efficient way to interact with foreign memory helps to bring the JVM one step closer to the goals of Project Panama and to a future Foreign Function Interface enhancement as outlined in JEP 191 or a future revision thereof.

Back to top

How Does the Foreign-Memory Access API Work?

The API introduces three main abstractions: 

  • MemorySegment - modelling a continuous region of memory.
  • MemoryAddress - a location within a given memory segment.
  • MemoryLayout - a way to define the layout of a memory segment in a language neutral fashion.

Since the API is in an Incubator module, you need to add it the jdk.incubator.foreign module to your module-info.java file (or use the --add-modules JVM argument).

Using MemorySegment

Memory segments represents a continuous region of memory and can be backed by either heap memory or off-heap memory. Factory methods exist to create a MemorySegment backed by primitive arrays, ByteBuffers, memory-mapped regions of a file, or to allocate a new off-heap segment.

To ensure a timely release of off-heap memory and other resources, MemorySegment implements AutoCloseable and can thus be used with try-with-resources to ensure the resources are freed when no longer needed. Trying to access a MemorySegment after it has been closed will result in an IllegalStateException.

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
  ...
}

A MemorySegment is bound to a specific thread, generally the current Java thread on creation. Should another thread want to interact with a memory segment, they need to first acquire an “acquired memory segment” from it by calling the acquire() method. A MemorySegment cannot be closed until all acquired memory segments from it has been closed first, so a try-with-resource approach around an acquired memory segment would likewise be appropriate.

try (MemorySegment acquired = segment().acquire()) {
  ...
}

A read-only view of the segment can be obtained by using the segment.asReadOnly() method. Likewise, a view of a sub-segment can obtained by using the segment.asSlice(offset, length) method. In both cases, closing the view will also close the underlying segment.

Using MemoryAddress

MemoryAddress encodes an offset within a given MemorySegment and are commonly obtained by calling the segment.baseAddress() method (which would be offset 0). A MemoryAddress is tied to a single MemorySegment, but although it’s thread-safe it cannot be used directly by multiple threads, as they will need to acquire() the associated segment first, and get the appropriate address from the acquired memory segment:

try (MemorySegment acquired = address.segment().acquire()) {
  MemoryAddress address = acquired.baseAddress().addOffset(address.offset());
  ...
}

Memory addresses are primarily used together with VarHandles obtained from MemoryHandles or from MemoryLayout, in order to access the underlying data.

For example, to access a memory segment as a sequence of ints stored in big endian byte order, and write data to it, it could be achieved as such:

VarHandle handle = MemoryHandles.varHandle(int.class, ByteOrder.BIG_ENDIAN);
for (int i = 0; i < 32; i++) {
  handle.set(segment.baseAddress().addOffset(i * Integer.BYTES), 1 << i);
}

 

Using MemoryLayout

Accessing a memory segment purely by an offset might not be the most natural way of reading data, as most often data is organized in some form of layout. MemoryLayout is there to describe such a layout in a language-neutral fashion.

A memory layout can be a simple value declared by size and byte order, for instance a 32-bit value in little endian, a 64-bit value in native order, etc. the MemoryLayouts class has many of these simple types predefined, such as MemoryLayouts.BITS_32_LE and MemoryLayouts.JAVA_LONG.

Layouts are often more complex than that, and MemoryLayout supports ways to combine values into sequences, structs and unions or order to describe these more complex layouts.

For instance a layout that is equivalent to an int[] could be declared as:

SequenceLayout layout = MemoryLayout.ofSequence(MemoryLayouts.JAVA_INT);

Data is rarely just laid out as a sequence of numbers, more often it is structured. For instance, a point, with two integers representing x and y coordinates, could be declared as:

GroupLayout pointLayout = MemoryLayout.ofStruct(
    MemoryLayouts.JAVA_INT,
    MemoryLayouts.JAVA_INT
);

For convenience and to ease readability and maintainability, memory layouts support naming of the individual elements. The above pointLayout can thus be improved as:

GroupLayout pointLayout = MemoryLayout.ofStruct(
    MemoryLayouts.JAVA_INT.withName("x"),
    MemoryLayouts.JAVA_INT.withName("y")
);

Memory layouts can be nested, although it increases the complexity of accessing the data as well. For instance, a triangle can be expressed as a sequence of three points:

SequenceLayout triangleLayout = MemoryLayout.ofSequence(3, pointLayout);

Accessing Individual Elements with varHandle()

In order to access data described using memory layouts, a varHandle can be created to access the individual elements. To declare which element should be access, a series of PathElements is used to describe the path to get from the root to the desired element. In the above triangle example, which could be thought of as an array of Points, an index into the sequence is needed, and a selector for which property of the Point is desired.

VarHandles are created using the varHandle() method, with the desired primitive type and the list of PathElements describing the path needed. For instance, accessing the x and y properties of the individual Points in the triangle can be declared as:

VarHandle varX = triangleLayout.varHandle(int.class,
    MemoryLayout.PathElement.sequenceElement(),
    MemoryLayout.PathElement.groupElement("x"));

VarHandle varY = triangleLayout.varHandle(int.class,
    MemoryLayout.PathElement.sequenceElement(),
    MemoryLayout.PathElement.groupElement("y"));

The varHandles can then be used to read from and write to the MemorySegment. For instance, setting the coordinates of the 2nd point (index 1) of a triangle to (10, 30) would then be:

varX.set(triangleSegment.baseAddress(), 1, 10);
varY.set(triangleSegment.baseAddress(), 1, 30);
Back to top

Final Thoughts

The Foreign-Memory Access API introduces a set of tools to describe and access memory segments in a more ordered fashion. Future improvements to the API that would allow mapping of records or inline classes that only contain primitive types to MemoryLayouts could be very appealing, especially considering the introspective capabilities of records. Adding support for reading and writing structured data from memory mapped files and memory segments could become a lot simpler in the future.

Additional Resources

Looking for further information on recent JDK Enhancement Proposals? Be sure to check out our recent blogs.

Blogs

Webinars

Or, if you want to see an in-depth breakdown of JEP 325 and 326, please check out my webinar on these JDK enhancement proposals:

See the Webinar

Back to top