JVM Platform
Buffer on JVM wraps java.nio.ByteBuffer for optimal performance.
Implementation
| Factory | JVM Type |
|---|---|
managed() | HeapByteBuffer |
Default | DirectByteBuffer |
shared() | Falls back to Default |
Direct vs Heap Buffers
Direct Buffers (Default)
val buffer = BufferFactory.Default.allocate(1024)
- On JVM 21+, allocated outside JVM heap via the FFM API (
Arena.ofAuto()); on pre-21 JVMs, falls back toByteBuffer.allocateDirect() - Zero-copy I/O with NIO channels
- Minimal / No GC overhead
- Best for network and file I/O
Heap Buffers
val buffer = BufferFactory.managed().allocate(1024)
- Allocated on JVM heap
- Managed by garbage collector
- May require copying for native I/O
- Good for short-lived, small buffers
Performance Characteristics
Operation | Direct | Heap
-------------------|------------|------------
Allocation | Slower | Faster
First access | Slower | Faster
Sustained I/O | Faster | Slower (copies)
GC impact | None | GC pauses
Memory limit | OS limit | -Xmx limit
JNI Considerations
When passing buffers to JNI:
// Direct buffers: zero-copy
val direct = BufferFactory.Default.allocate(1024)
nativeFunction(direct.toNativeData().byteBuffer) // No copy
// Heap buffers: may copy
val heap = BufferFactory.managed().allocate(1024)
nativeFunction(heap.toMutableNativeData().byteBuffer) // JVM may copy to temp direct buffer
Accessing Underlying ByteBuffer
BufferFactory.Default does not always return JvmBuffer — it returns FfmAutoBuffer on JVM 21+ and DirectJvmBuffer on older JVMs, both of which extend BaseJvmBuffer. Casting straight to JvmBuffer will throw a ClassCastException. Cast to BaseJvmBuffer instead, or prefer toNativeData()/toMutableNativeData() (shown below), which work regardless of the concrete implementation:
val buffer = BufferFactory.Default.allocate(1024) as BaseJvmBuffer
val nioBuffer: ByteBuffer = buffer.byteBuffer
// Use with NIO channels
channel.write(nioBuffer)
Native Data Conversion
Convert buffers to JVM-native ByteBuffer for NIO and JNI interop:
val buffer = BufferFactory.Default.allocate(1024)
buffer.writeBytes(data)
buffer.resetForRead()
// Get read-only direct ByteBuffer (zero-copy for direct buffers)
val nativeData = buffer.toNativeData()
val readOnlyBuffer: ByteBuffer = nativeData.byteBuffer
channel.write(readOnlyBuffer)
// Get mutable direct ByteBuffer (zero-copy for direct buffers)
val mutableData = buffer.toMutableNativeData()
val mutableBuffer: ByteBuffer = mutableData.byteBuffer
channel.read(mutableBuffer)
Zero-Copy Behavior
toNativeData() and toMutableNativeData() always return direct ByteBuffers for native memory access:
| Conversion | Heap Buffer | Direct Buffer |
|---|---|---|
toNativeData() | Copy to direct | Zero-copy (duplicate) |
toMutableNativeData() | Copy to direct | Zero-copy (duplicate) |
toByteArray() | Zero-copy (backing array) | Copy required |
For true zero-copy conversion, allocate with BufferFactory.Default (the default). Changes to the returned ByteBuffer will reflect in the original buffer and vice versa.
All conversion functions operate on remaining bytes (position to limit) and do not modify the buffer's position or limit.
See Platform Interop for more details.
Best Practices
- Use Direct for I/O - network sockets, file channels
- Use Heap for parsing - or pool Direct buffers
- Pool Direct buffers - allocation is expensive
- Watch off-heap memory - not tracked by
-Xmx