Introduction
In this post, we dive into a more advanced topic: pointer arithmetic in Java. With the introduction of the Foreign Function & Memory API (Panama), we can interact with native memory.
Usually, when we work with off-heap memory, we use MemorySegment instances to ensure safety. However, creating these
objects can sometimes add overhead. In this post, we will look at how to access native memory using addresses.
The goal is to create fewer objects that are not strictly needed and only add GC pressure.
Background Info
I am working on a Project that creates bindings to IO_Uring. The project
has a path that loops over thousands of pointers and converts them to MemorySegments. This is done to access three
values inside the struct the pointers point to. This loop creates a lot of short-lived Objects as I need a new
MemorySegment for each pointer. To prevent creating so many objects, I used the pointer arithmetic you see in this
post.
Warning
When using a global segment, we are trading safety for speed. This approach cannot detect if the memory backing the pointer has been released or even is there to begin with.
The Setup
To make this work, we need a way to access memory using addresses. In the following class, we define a simple “Point”
structure (with x and y coordinates) and a special constant called GLOBAL_MEMORY.
The GLOBAL_MEMORY acts as a view over the entire memory space. By creating a segment starting at address 0 and
explicitly allowing it to access a huge range, we can use any long address as an offset. This allows us to read
and write data just by knowing the memory address, bypassing the need to create a specific MemorySegment instance
for each pointer.
| |
The magic and the danger lies in the GLOBAL_MEMORY constant. We create a segment starting at address 0 and use
reinterpret to extend its size to Long.MAX_VALUE. This effectively gives us a view over the entire system memory.
Because our VarHandles are derived from the Layout, they expect a MemorySegment and an offset.
By passing GLOBAL_MEMORY as the base segment and the address as the offset, we are telling Java:
“Start at 0, move forward by address, and read the data.”
Comparing Approaches
Now let's see how this compares to the standard way of accessing off-heap memory. In the following example, we allocate a
ZeroGcPoint inside an Arena and try two different approaches to read the data.
Approach 1 represents the standard way. We take the address and reconstruct a MemorySegment from it. While safe and kind of the standard, this creates a new Java object, which adds pressure to the Garbage Collector.
Approach 2 uses our ZeroGcPoint class. We simply pass the long address. Because we are using the static
GLOBAL_MEMORY inside the utility class, no new MemorySegment object is created during the access. This is effectively
pointer arithmetic in Java.
| |
In the first approach, notice line 29 MemorySegment.ofAddress. We are explicitly asking the JVM to instantiate a new
MemorySegment object on the heap that wraps the native memory at that address. If you do this once, it's negligible.
If you do this inside a loop running thousands of times, you are generating a massive number of short-lived
objects that the Garbage Collector eventually has to clean up.
Benchmark
Running a JMH benchmark shows the following performance improvement:
| |
Conclusion
In this post, we looked at how to perform pointer arithmetic using the Foreign Function & Memory API. By using a
global memory segment and addresses, we can access native memory without the overhead of creating temporary
MemorySegments.