Written by:
David VlijmincxMemory segments were introduced as part of Java's Foreign Memory Access API to provide a safe and efficient way to
work with off-heap memory. In this article, I'll guide you through some concepts of memory segments and provide
practical examples of how to use them in your Java applications.
A memory segment represents a contiguous region of memory with well-defined boundaries and lifecycle. Unlike
ByteBuffers, memory segments offer better safety guarantees and more control over memory access.
Memory segments can be:
- Native segments (off-heap): Memory allocated outside the Java heap, useful for working with native code
- Heap segments: Memory that's actually part of the Java heap but accessed through the
segment API
Each segment has an address and a size. They can also be part of an Arena that controls its lifetime (when it will
be freed).
Let's take a look at the different ways to create memory segments:
Native segments are allocated outside the Java heap. This is useful for large buffers or long-lived data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| try (Arena arena = Arena.ofConfined()) {
// Allocate 1024 bytes of off-heap memory
// The arena manages the lifecycle of this memory - it will be freed when the arena is closed
MemorySegment segment = arena.allocate(1024);
// Fill the segment with ones
// This is more efficient than looping through each byte
segment.fill((byte) 1);
// At this point, we have a segment with 1024 ones inside of it
System.out.println("Allocated segment size: " + segment.byteSize()); // Will print 1024
}
// When we exit the try-with-resources block, the arena closes and the memory is automatically freed
// This prevents memory leaks
|
You can create a memory segment backed by a Java array. This is useful when you want to use the memory segment API
to interact with existing array data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Create a simple byte array with some data
byte[] bytes = {1, 2, 3, 4, 5};
// Create a memory segment backed by this array
MemorySegment segment = MemorySegment.ofArray(bytes);
// We can now use memory segment operations on this array
// When we modify the segment, we're actually modifying the backing array
segment.setAtIndex(ValueLayout.JAVA_BYTE, 0, (byte) 10);
// The original array has been modified
System.out.println(bytes[0]); // Outputs: 10
// We can also access the array elements through the segment
byte value = segment.get(ValueLayout.JAVA_BYTE, 2);
System.out.println("Value at index 2: " + value); // Outputs: 3
// The segment's size matches the array size
System.out.println("Segment size: " + segment.byteSize()); // Outputs: 5
|
The same approach works for other primitive array types like int[], long[], etc.
If you're working with code that uses ByteBuffers, you can convert them to memory segments:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Create a direct ByteBuffer (off-heap)
// Create a direct ByteBuffer (off-heap)
ByteBuffer buffer = ByteBuffer.allocateDirect(100).order(ByteOrder.nativeOrder());
buffer.putInt(0, 42); // Write a value to the buffer
// Create a memory segment that references the same memory
// Again, this doesn't copy any data
MemorySegment segment = MemorySegment.ofBuffer(buffer);
// We can now use the memory segment API to access the same memory
int value = segment.get(ValueLayout.JAVA_INT, 0);
System.out.println("Value from segment: " + value); // Outputs: 42
// Modifying the segment affects the buffer
segment.set(ValueLayout.JAVA_INT, 0, 24);
System.out.println("Value from buffer: " + buffer.getInt(0)); // Outputs: 24
// The segment's size matches the buffer's capacity
System.out.println("Segment size: " + segment.byteSize()); // Outputs: 100
|
This interoperability is useful when migrating code from the old ByteBuffer API to the new memory segment API.
Reading and Writing values
Memory segments provide type-safe methods for reading and writing primitive values. These operations specify both
the type layout and the offset within the segment.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| try (Arena arena = Arena.ofConfined()) {
// Allocate space for several integers
// Each integer takes 4 bytes, so we allocate enough space for multiple values
MemorySegment segment = arena.allocate(100);
// Write an integer at offset 0
// at the beginning of our segment
segment.set(ValueLayout.JAVA_INT, 0, 42);
// Read it back - must use the same layout to get the correct interpretation of bytes
int value = segment.get(ValueLayout.JAVA_INT, 0);
System.out.println("Integer value: " + value); // Outputs: 42
// Using index-based access (more readable for array-like access)
// Instead of calculating the byte offset manually, we can use indexes
// This code writes to the second integer position (index 1)
segment.setAtIndex(ValueLayout.JAVA_INT, 1, 24);
// Read back using the same index-based approach
int anotherValue = segment.getAtIndex(ValueLayout.JAVA_INT, 1);
System.out.println("Second integer: " + anotherValue); // Outputs: 24
// We can also write other primitive types
segment.setAtIndex(ValueLayout.JAVA_DOUBLE, 3, 3.14159);
double pi = segment.getAtIndex(ValueLayout.JAVA_DOUBLE, 3);
System.out.println("Double value: " + pi); // Outputs: 3.14159
}
|
The key advantage here is that the code clearly specifies the type of data being written or read, which makes the
code more self-documenting and less error-prone compared to using raw byte offsets.
Memory segments provide convenient methods for reading and writing strings. These methods handle the encoding and
null-termination automatically.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| try (Arena arena = Arena.ofConfined()) {
// Allocate a segment large enough for our strings
// We need to ensure enough space for the characters plus null-termination
MemorySegment segment = arena.allocate(100);
// Write a null-terminated string using UTF-8 (default encoding)
// The method handles converting the Java String to bytes and adding the null terminator
segment.setString(0, "Hello, World!");
// Read it back - this automatically reads until a null byte is found
String result = segment.getString(0);
System.out.println("String value: " + result); // Outputs: Hello, World!
// We can see the actual bytes if we want to inspect the encoding
for (int i = 0; i < "Hello, World!".length() + 1; i++) {
byte b = segment.get(ValueLayout.JAVA_BYTE, i);
System.out.print(b + " ");
}
System.out.println(); // The last byte will be 0 (null terminator)
// Using a different charset for internationalized text
// Some characters require multiple bytes in certain encodings
segment.setString(50, "こんにちは", StandardCharsets.UTF_16);
String japanese = segment.getString(50, StandardCharsets.UTF_16);
System.out.println("Japanese greeting: " + japanese); // Outputs: こんにちは
}
|
This is especially valuable when interacting with native code that expects null-terminated strings in
specific encodings.
You can create views of portions of a segment using the asSlice method. This is useful for working with structured
data or partitioning a large memory region.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| try (Arena arena = Arena.ofConfined()) {
// Create a segment with 100 bytes
MemorySegment segment = arena.allocate(100);
// Fill the segment with a pattern for this example
// Each byte contains its index value
for (int i = 0; i < 100; i++) {
segment.set(ValueLayout.JAVA_BYTE, i, (byte) i);
}
// Create a slice starting at offset 10 with length 20
// This creates a view into the original segment - no memory is copied
MemorySegment slice = segment.asSlice(10, 20);
// The slice references the same memory, but has its own bounds
// When we read from offset 0 in the slice, we're reading from offset 10 in the original
System.out.println("First byte of slice: " + slice.get(ValueLayout.JAVA_BYTE, 0)); // Outputs: 10
System.out.println("Slice size: " + slice.byteSize()); // Outputs: 20
// Modify through the slice - this affects the original segment
slice.fill((byte) 42);
// Check that the original segment was modified
System.out.println("Byte at offset 10 in original: " + segment.get(ValueLayout.JAVA_BYTE, 10)); // Outputs: 42
System.out.println("Byte at offset 29 in original: " + segment.get(ValueLayout.JAVA_BYTE, 29)); // Outputs: 42
System.out.println("Byte at offset 30 in original: " + segment.get(ValueLayout.JAVA_BYTE, 30)); // Outputs: 30 (unchanged)
// Trying to access outside the slice bounds will throw an exception
try {
slice.get(ValueLayout.JAVA_BYTE, 20); // This will fail
} catch (IndexOutOfBoundsException e) {
System.out.println("Caught expected exception: " + e.getMessage());
}
}
|
Slices are powerful for working with complex data structures or processing large buffers in smaller chunks without
doing new memory allocations.
Memory segments provide efficient bulk operations for copying data between segments. These operations are optimized
for performance and can be significantly faster than byte-by-byte copying.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| try (Arena arena = Arena.ofConfined()) {
// Create a source segment and fill it with a value
MemorySegment source = arena.allocate(100);
source.fill((byte) 1); // All bytes are set to 1
// Create a destination segment
MemorySegment destination = arena.allocate(100);
// Initially all bytes are 0
// Copy 50 bytes from source to destination
// This performs a direct memory copy, which is very efficient
MemorySegment.copy(source, 0, destination, 0, 50);
// Verify the copy - first 50 bytes should be 1, rest should be 0
System.out.println("Byte at index 0: " + destination.get(ValueLayout.JAVA_BYTE, 0)); // Outputs: 1
System.out.println("Byte at index 49: " + destination.get(ValueLayout.JAVA_BYTE, 49)); // Outputs: 1
System.out.println("Byte at index 50: " + destination.get(ValueLayout.JAVA_BYTE, 50)); // Outputs: 0
// Alternative method using the destination's copyFrom method
// This accomplishes the same thing as the previous copy
destination.fill((byte) 0); // Reset destination
destination.copyFrom(source.asSlice(0, 50));
// Verify again
System.out.println("Byte at index 0 after copyFrom: " + destination.get(ValueLayout.JAVA_BYTE, 0)); // Outputs: 1
System.out.println("Byte at index 50 after copyFrom: " + destination.get(ValueLayout.JAVA_BYTE, 50)); // Outputs: 0
// Finding differences between segments
// After copying the first 50 bytes, the segments differ starting at index 50
long mismatchOffset = source.mismatch(destination);
System.out.println("First mismatch at offset: " + mismatchOffset); // Outputs: 50
// If we make the segments identical, mismatch returns -1
destination.copyFrom(source);
mismatchOffset = source.mismatch(destination);
System.out.println("Mismatch after full copy: " + mismatchOffset); // Outputs: -1 (no mismatch)
}
|
These bulk operations are useful for high-performance scenarios where large amounts of data need to be moved
efficiently.
You can efficiently copy between segments and arrays, which is useful when interfacing between traditional Java code
and code that uses memory segments:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // Create an array of integers
int[] array = {1, 2, 3, 4, 5};
try (Arena arena = Arena.ofConfined()) {
// Allocate a segment large enough to hold the array
MemorySegment segment = arena.allocate(ValueLayout.JAVA_INT, array.length);
// Copy from the Java array to the memory segment
// This copies all integers from the array to the segment with proper layout
MemorySegment.copy(array, 0, segment, ValueLayout.JAVA_INT, 0, array.length);
// Verify the copy worked by reading individual values
for (int i = 0; i < array.length; i++) {
int value = segment.getAtIndex(ValueLayout.JAVA_INT, i);
System.out.println("Value at index " + i + ": " + value);
}
// Modify the segment
segment.setAtIndex(ValueLayout.JAVA_INT, 0, 42);
// Copy to a new array
// This creates a new array and populates it with values from the segment
int[] newArray = segment.toArray(ValueLayout.JAVA_INT);
System.out.println("New array values: " + Arrays.toString(newArray)); // Outputs: [42, 2, 3, 4, 5]
// Note that the original array is unchanged
System.out.println("Original array values: " + Arrays.toString(array)); // Outputs: [1, 2, 3, 4, 5]
// We can also copy into an existing array
int[] targetArray = new int[5];
MemorySegment.copy(segment, ValueLayout.JAVA_INT, 0, targetArray, 0, 5);
System.out.println("Target array values: " + Arrays.toString(targetArray)); // Outputs: [42, 2, 3, 4, 5]
}
|
These methods provide an efficient bridge between traditional Java array-based code and the new memory segment API.
When working with structured data, you can use MemoryLayout to define the structure. This helps with both
documentation and typed access to fields:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // Define a point structure with x and y coordinates
// This is similar to defining a C struct
StructLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
try (Arena arena = Arena.ofConfined()) {
// Allocate memory according to the layout
// The size will be determined by the layout
MemorySegment point = arena.allocate(pointLayout);
// Create VarHandles for accessing fields by name
// These provide type-safe access to the fields of our structure
VarHandle xHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("y"));
// Set values using the VarHandles
// This is similar to setting fields in a Java object, but we're working with raw memory
xHandle.set(point, 0, 10);
yHandle.set(point, 0, 20);
// Get values using the VarHandles
int x = (int) xHandle.get(point, 0);
int y = (int) yHandle.get(point, 0);
System.out.println("Point: (" + x + ", " + y + ")"); // Outputs: Point: (10, 20)
// We can create multiple instances of our structure
MemorySegment anotherPoint = arena.allocate(pointLayout);
xHandle.set(anotherPoint, 0, 30);
yHandle.set(anotherPoint, 0, 40);
// We could also access fields by offset, but it's more error-prone
// The layout approach is safer and more self-documenting
point.set(ValueLayout.JAVA_INT, 0, 15); // Set x to 15 directly
System.out.println("New x value: " + (int) xHandle.get(point, 0)); // Outputs: 15
}
|
For complex data structures, especially when interacting with native code, memory layouts provide a safer and more
maintainable approach than using raw offsets.
Memory segments are associated with a scope that controls their lifecycle. The scope determines when the segment's
memory is freed and which threads can access it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| try (Arena arena = Arena.ofConfined()) {
// This segment is confined to the current thread
// Other threads cannot access it, which prevents concurrent modification issues
MemorySegment segment = arena.allocate(100);
// Check if the segment is accessible from the current thread
boolean accessible = segment.isAccessibleBy(Thread.currentThread());
System.out.println("Accessible by current thread: " + accessible); // true
// Try to access from another thread
Thread otherThread = new Thread(() -> {
try {
// This will throw an exception because the segment is confined to the original thread
segment.fill((byte) 1);
System.out.println("This line won't be reached");
} catch (WrongThreadException e) {
System.out.println("Expected exception: " + e.getMessage());
}
});
otherThread.start();
otherThread.join();
// We can still access the segment from the original thread
segment.fill((byte) 2);
System.out.println("Successfully wrote to segment from original thread");
// When the arena is closed (at the end of try-with-resources), the segment becomes inaccessible
}
// Attempting to access segment here would throw an exception because the arena is closed
|
Using a shared arena allows access from multiple threads:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| try (Arena arena = Arena.ofShared()) {
// This segment can be accessed from any thread
MemorySegment segment = arena.allocate(100);
// Launch multiple threads to access the segment concurrently
Thread thread1 = new Thread(() -> {
// This is allowed because the segment is in a shared arena
segment.setAtIndex(ValueLayout.JAVA_INT, 0, 42);
System.out.println("Thread 1 wrote value 42");
});
Thread thread2 = new Thread(() -> {
// This thread can also access the segment
try {
// Add a small delay to ensure thread1 runs first
Thread.sleep(10);
int value = segment.getAtIndex(ValueLayout.JAVA_INT, 0);
System.out.println("Thread 2 read value: " + value); // Should be 42
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// Note: When using shared segments, it's your responsibility to synchronize access
// if multiple threads might write to the same memory locations
}
|
If you want to know more about arenas, see this post.
When working with memory segments, keep these performance tips in mind:
- Reuse segments when possible rather than creating new ones for each operation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Inefficient: Creating a new segment for each operation
for (int i = 0; i < 1000; i++) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(1024);
// Do something with segment
}
}
// More efficient: Reuse a single segment
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(1024);
for (int i = 0; i < 1000; i++) {
// Do something with segment
segment.fill((byte) 0); // Reset the segment for next iteration if needed
}
}
|
- Use bulk operations rather than single-element access for large data sets:
1
2
3
4
5
6
7
8
| // Inefficient: Byte-by-byte copying
for (int i = 0; i < sourceSegment.byteSize(); i++) {
byte value = sourceSegment.get(ValueLayout.JAVA_BYTE, i);
destSegment.set(ValueLayout.JAVA_BYTE, i, value);
}
// More efficient: Bulk copy
destSegment.copyFrom(sourceSegment);
|
- Choose the right scope every scope has different characteristics.
1
2
3
4
5
6
7
8
9
10
11
| // Use confined arenas when only one thread needs access
try (Arena arena = Arena.ofConfined()) {
MemorySegment segment = arena.allocate(1024);
// Do single-threaded work
}
// Only use shared arenas when truly necessary
try (Arena arena = Arena.ofShared()) {
MemorySegment segment = arena.allocate(1024);
// Do multi-threaded work
}
|
- Consider alignment for performance-critical applications:
1
2
| // Aligned allocation for better performance with certain hardware
MemorySegment segment = arena.allocate(1024, 64); // 64-byte alignment
|
- Use native segments for off-heap operations that need to avoid GC overhead:
1
2
3
4
5
| // For large, long-lived buffers, use native segments to avoid heap pressure
try (Arena arena = Arena.ofConfined()) {
MemorySegment largeBuffer = arena.allocate(1024 * 1024 * 100); // 100 MB buffer off-heap
// This won't affect GC performance
}
|
Error Handling
Memory segments provide better error handling than direct ByteBuffers, which helps catch memory-related bugs earlier:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| try (Arena arena = Arena.ofConfined()) {
// Allocate a small segment to demonstrate bounds checking
MemorySegment segment = arena.allocate(4); // Only 4 bytes
try {
// This will throw an exception instead of causing undefined behavior
// Long is 8 bytes, but segment is only 4 bytes
segment.get(ValueLayout.JAVA_LONG, 0);
System.out.println("This line won't be reached");
} catch (IndexOutOfBoundsException e) {
System.out.println("Caught bounds exception: " + e.getMessage());
// This allows us to diagnose and fix the problem during development
}
try {
// This will also throw an exception - accessing beyond the end
segment.get(ValueLayout.JAVA_INT, 1); // 4 bytes starting at offset 1 = out of bounds
} catch (IndexOutOfBoundsException e) {
System.out.println("Caught another bounds exception: " + e.getMessage());
}
// This is valid - fully within bounds
segment.set(ValueLayout.JAVA_INT, 0, 42);
System.out.println("Successfully wrote an int: " + segment.get(ValueLayout.JAVA_INT, 0));
}
|
These safety features help catch memory access errors during development rather than experiencing crashes or corruption in production.
Memory segments are particularly useful when integrating with native code through the Foreign Function Interface:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| try (Arena arena = Arena.ofConfined()) {
// Allocate memory for native function to work with
MemorySegment buffer = arena.allocate(1024);
// Get the address to pass to native code
// This is the raw memory address that native code can understand
long address = buffer.address();
System.out.println("Buffer address: " + Long.toHexString(address));
// Set up to call a native function (strlen in this example)
// First, we need to get a handle to the function
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
// Set a string in our buffer
// The native strlen function expects a null-terminated string
buffer.setString(0, "Hello, Native World!");
try {
// Call the native function with our buffer
// This is passing control to native code that will calculate the string length
long length = (long) strlen.invoke(buffer);
System.out.println("String length reported by native strlen: " + length);
// We can verify the result
System.out.println("Actual string length: " + "Hello, Native World!".length());
} catch (Throwable e) {
e.printStackTrace();
}
// We can also read back data that native code may have written to our buffer
}
|
This integration with native code is cleaner, safer, and more efficient than the JNI (Java Native Interface).
Memory segments provide a powerful and safe way to work with memory in Java, especially when performance or native
interoperability is important. By understanding how to create, manipulate, and manage the lifecycle of memory segments, you can write more efficient and reliable code.
The most important benefits are:
- Direct access to off-heap memory for better performance and reduced GC pressure
- Better safety guarantees than ByteBuffer through bounds checking and scope controls
- Cleaner integration with native code
- Type-safe access to structured data
- Efficient bulk operations for data copying and transformation
By following the patterns and practices outlined in this article, you'll be well-equipped to use memory
segments in your Java applications.