Low latency java

Table of Contents

Java NIO Direct ByteBuffer

In Java NIO,ByteBuffer.allocateDirect(int capacity) creates a direct byte buffer, which is allocated in native (off-heap) memory rather than on the JVM heap. This memory is managed outside of the Java Garbage Collector.

Internally, the JVM uses native calls (e.g.,malloc) to allocate this memory. A small Java object acts as a wrapper around the native region. When the wrapper becomes unreachable, a Cleaner mechanism is used to eventually release the underlying memory.

Because direct buffers live outside the heap, they are not reclaimed during normal GC cycles. This can improve GC performance, but also means memory may not be freed promptly. Excessive allocation without reuse may lead to OutOfMemoryError: Direct buffer memory.

The main advantage of direct buffers is more efficient I/O. They allow the JVM to pass memory directly to native I/O operations (e.g., file or socket reads/writes) without copying data between the Java heap and native memory.

In contrast, heap buffers created with ByteBuffer.allocate() often require an extra copy into a temporary native buffer before I/O. Direct buffers eliminate this overhead, making them better suited for large or high-throughput workloads.

A newly created direct buffer starts in write mode, meaning it is immediately ready to be filled with data. Its position is initialized to 0, indicating that the next read or write will occur at the very beginning of the buffer. The limit is set to the buffer's full capacity, defining the maximum number of bytes that can be written. Data is typically written using put(), and once writing is complete, the buffer is switched to read mode (e.g., via flip()) so that data can be read sequentially using get().

Unlike heap buffers, direct buffers do not guarantee access to a backing Java array. Heap buffers are internally backed by a byte[], allowing direct access via array(). In contrast, direct buffers reside in native memory outside the JVM, so there is no accessible Java array representation. As a result, calling array() on a direct buffer may throw an exception. Instead, data must be accessed through buffer methods like get() and put(), or by passing the buffer directly to native I/O operations.

While direct buffers enable a "zero-copy" style of I/O (avoiding extra memory copies within the JVM), they are more expensive to allocate and deallocate. As a result, they are best used for large, long-lived buffers that are reused, such as in high-performance networking or file I/O systems.

Best practice: Avoid frequent allocation of direct buffers. Prefer reusing buffers or using pooling strategies to reduce native memory overhead and improve performance stability.