Monday, February 19, 2024

Foreign-memory access API: Performance off the heap

5 min read

In 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.

You might want to check these out ↘

How Your Database Stands Up to the Ultimate Reliability Check: ACID rules

How Your Database Stands Up to the Ultimate Reliability Check: ACID rules

For IT professionals in database management, grasping the core principles that protect our digital transactions is essential. Today, we're exploring the ACID model, a pivotal framework for ensuring transaction reliability and security. This goes beyond mere theoretical knowledge; it's vital for the smooth operation of diverse systems, from financial processes to social media platforms. We'll delve into the ACID principles and their critical role in real-world applications.
Convert String to Date: The Java Ways

Convert String to Date: The Java Ways

Converting strings to dates in programming is a common task that's more complex than it seems. This challenge is rooted in the wide range of date formats and the specifics of time zones. Converting a String to a Date in Java can be done in several ways, depending on the Java version you're using and whether you are incorporating external libraries. Below are the primary methods used in different versions of Java and through some commonly used external libraries.
Cookbook: How to Effectively Revert a Git Commit

Cookbook: How to Effectively Revert a Git Commit

As a software engineer, encountering situations where recent changes need to be undone is not uncommon. It may be due to errors, or in some cases just need a significant rework. When such scenarios arise while using Git as your version control system, there’s a few approaches that can help revert those changes effectively. Let’s take a closer look.
Contact Us

Stay ahead of the curve
on software insights and technology trends.