Foreign-memory access API: Performance off the heap
5 min readIn Java development, efficient memory management is critical, especially for handling large datasets or interacting with native libraries. The Foreign-Memory Access API, introduced in Java 14, aim to significantly improve how developers work with memory outside the Java heap, by offering a better alternative to the complex and error-prone Java Native Interface (JNI).
1. Why Foreign-Memory Access
Java developers traditionally depend on the garbage-collected heap for memory management. This method, while safe, is not always efficient for high-performance tasks involving large data structures or native operations due to garbage collection latency.
On the other hand, JNI, used for native memory interaction, is complex and increases the risk of native crashes.
This is where foreign-memory access steps in, offering a practical solution.
2. Introducing the Foreign-Memory Access API
Part of Project Panama, the Foreign-Memory Access API provides a safer and more efficient way to access memory outside the Java heap, such as native memory or memory-mapped files. This API ensures safety checks to avoid JVM crashes, offers better performance by avoiding JNI overhead, and allows developers to explicitly control the memory lifecycle.
2.1 Key Features
Some key features of the foreign-memory API include:
- Safety: Prevents JVM crashes with built-in safety checks.
- Efficiency: Reduces overhead compared to JNI.
- Scope Control: Enables explicit memory lifecycle management.
2.2 API
Some of the most important APIs on the foreign-memory access are listed here. For a complete list of API, please refer to the java documentation
2.2.1. MemorySegment
MemorySegment
represents a contiguous memory region. It can be used to allocate memory either on the Java heap (managed) or off-heap (native). It ensures type safety and spatial safety, preventing illegal access outside the memory segment.
try (MemorySegment segment = MemorySegment.allocateNative(100)) { // Allocate 100 bytes of native memory
MemoryAccess.setInt(segment, 0, 123); // Write an int at offset 0
}
2.2.2. MemoryAddress
MemoryAddress
represents a native memory address. It can be used to represent a pointer in native code, allowing direct memory access and manipulation.
MemorySegment segment = MemorySegment.allocateNative(100);
MemoryAddress address = segment.address();
MemoryAccess.setIntAtOffset(segment, address.addOffset(10).toRawLongValue(), 456); // Write an int 10 bytes from the start
2.2.3. MemoryAccess
MemoryAccess
provides static methods to read and write primitive values from/to a memory segment. It supports all Java primitive types and ensures data is accessed safely and efficiently.
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
MemoryAccess.setInt(segment, 0, 789); // Write an int
int value = MemoryAccess.getInt(segment, 0); // Read an int
}
2.2.4. SegmentAllocator
SegmentAllocator
facilitates allocation of memory segments according to a specific allocation strategy. It can be used to manage memory more efficiently, especially in scenarios where multiple memory segments need to be allocated and deallocated frequently.
SegmentAllocator allocator = SegmentAllocator.arenaAllocator(MemorySegment.allocateNative(1024));
try (MemorySegment segment = allocator.allocate(100)) { // Allocate 100 bytes within the arena
MemoryAccess.setInt(segment, 0, 1234);
}
2.2.5. ResourceScope
ResourceScope
manages the lifecycle of one or more memory segments, ensuring that the memory is released when no longer needed. It's particularly useful for managing the deallocation of native memory in a deterministic manner.
try (ResourceScope scope = ResourceScope.newConfinedScope()) {
MemorySegment segment = MemorySegment.allocateNative(100, scope);
MemoryAccess.setInt(segment, 0, 5678);
// The segment will be automatically deallocated when the scope is closed
}
3. Comparison with Traditional Ways
Previously, developers had to choose between heap memory (which is safe but slow due to garbage collection), ByteBuffer (which is faster but unsafe), and JNI (which is flexible but complex). The Foreign-Memory Access API offers a balanced solution, providing both safety and performance.
3.1 Use ByteBuffer
Before the Foreign-Memory Access API, one common method to work with native memory in Java was through the ByteBuffer class and its allocateDirect method. This approach bypasses the garbage-collected heap but lacks the safety and ease of use provided by the newer API. Here's an example:
import java.nio.ByteBuffer;
public void readWriteBuffer() {
// Allocate a direct ByteBuffer of 100 bytes
ByteBuffer buffer = ByteBuffer.allocateDirect(100);
// Write an integer (4 bytes) into the buffer at position 0
buffer.putInt(0, 123);
// Read the integer from the buffer
int value = buffer.getInt(0);
}
3.2 Use foreign-memory access API
The Foreign-Memory Access API provides a more robust and safer way to handle native memory. It introduces MemorySegment for representing contiguous memory regions and MemoryAccess for performing safe memory operations, along with safety checks and a more intuitive API design.
import jdk.incubator.foreign.MemoryAccess;
import jdk.incubator.foreign.MemorySegment;
public void runForeignMemoryAccess() {
try (MemorySegment segment = MemorySegment.allocateNative(100)) { // Allocate 100 bytes of native memory
// Write an integer (4 bytes) into the memory segment at offset 0
MemoryAccess.setInt(segment, 0, 123);
// Read the integer from the memory segment
int value = MemoryAccess.getInt(segment, 0);
}
}
3.3 Comparison
- Safety: The ByteBuffer approach provides limited safety checks, making it prone to errors such as buffer underflow or overflow. The Foreign-Memory Access API, on the other hand, includes comprehensive safety checks to prevent such issues.
- Performance: Both methods avoid the overhead of garbage collection. However, the Foreign-Memory Access API is designed to be more efficient in managing and accessing native memory.
- Ease of Use: The Foreign-Memory Access API offers a more straightforward and flexible approach for working with native memory, making it easier to use and reducing the risk of errors compared to managing ByteBuffers or using JNI directly.
In summary, the Foreign-Memory Access API provides Java developers with a new way to efficiently and safely interact with memory outside the Java heap. This advancement simplifies working with large data sets and native libraries by offering a direct, less error-prone alternative to JNI. It enhances Java's capabilities for performance-critical applications.