Java ByteBuffer: NIO Direct and Heap Buffers
ByteBuffer is the central abstraction in Java NIO for high-performance binary I/O. Unlike streams, it lets you read and write directly into a fixed-size buffer, control byte order, and optionally bypass the JVM heap altogether with direct (native memory) buffers: essential for network programming, binary file parsing, and interoperability with native code.
Contents
Buffer Properties: capacity, limit, position, mark
Every ByteBuffer
- capacity: total size of the buffer (fixed at creation, never changes).
- limit: the boundary beyond which data should not be read or written.
- position: the index of the next byte to read or write (advances automatically).
- mark: a saved position; calling
restores position to mark.reset()
Invariant: 0 β€ mark β€ position β€ limit β€ capacity
import java.nio.ByteBuffer;
// Allocate a heap buffer of 8 bytes
ByteBuffer buf = ByteBuffer.allocate(8);
// capacity=8, limit=8, position=0, mark=-1
System.out.println("capacity: " + buf.capacity()); // 8
System.out.println("limit: " + buf.limit()); // 8
System.out.println("position: " + buf.position()); // 0
System.out.println("remaining:" + buf.remaining()); // limit - position = 8
flip(), clear(), rewind(), compact()
The writeβread transition is the most common source of bugs with ByteBuffer. You write data into the buffer, then call flip()
ByteBuffer buf = ByteBuffer.allocate(8);
// --- Write phase ---
buf.put((byte) 10);
buf.put((byte) 20);
buf.put((byte) 30);
// position=3, limit=8
// Prepare for reading: sets limit=position, position=0
buf.flip();
// position=0, limit=3 β "3 bytes available to read"
// --- Read phase ---
while (buf.hasRemaining()) {
System.out.println(buf.get()); // 10, 20, 30
}
// position=3, limit=3
// ---- Reset for reuse ----
// clear() β resets for fresh write (discards unread data)
buf.clear();
// position=0, limit=8 (capacity)
// rewind() β re-read from the start without clearing
buf.rewind();
// position=0, limit unchanged
// compact() β shift unread bytes to start, prepare for more writing
buf.flip(); // back to read mode first
buf.get(); // consume one byte
buf.compact();
// Unread bytes shifted to [0..N-1], position=N, limit=capacity
flip()get() and put() Operations
ByteBufferpositionpositionput()flip()limit = positionposition = 0rewind()put(byte[])get(byte[])
ByteBuffer buf = ByteBuffer.allocate(32);
// Relative put β advances position
buf.put((byte) 1);
buf.putInt(42); // writes 4 bytes
buf.putLong(123456789L); // writes 8 bytes
buf.putDouble(3.14); // writes 8 bytes
buf.putShort((short) 999);
buf.flip();
// Relative get β advances position
byte b = buf.get(); // 1
int i = buf.getInt(); // 42
long l = buf.getLong(); // 123456789
double d = buf.getDouble(); // 3.14
short s = buf.getShort(); // 999
// Absolute get/put β does NOT advance position
ByteBuffer buf2 = ByteBuffer.allocate(8);
buf2.putInt(0, 0xDEADBEEF); // write at index 0
buf2.putInt(4, 0xCAFEBABE); // write at index 4
System.out.printf("%08X%n", buf2.getInt(0)); // DEADBEEF
System.out.printf("%08X%n", buf2.getInt(4)); // CAFEBABE
System.out.println(buf2.position()); // still 0
// Bulk put/get
byte[] data = {1, 2, 3, 4, 5};
ByteBuffer buf3 = ByteBuffer.allocate(10);
buf3.put(data);
buf3.flip();
byte[] out = new byte[5];
buf3.get(out);
System.out.println(Arrays.toString(out)); // [1, 2, 3, 4, 5]
Byte Order
Multi-byte values (int, long, double, etc.) can be stored in big-endian (network byte order) or little-endian order. The default is BIG_ENDIAN
ByteBuffer buf = ByteBuffer.allocate(4);
// Default: BIG_ENDIAN (most significant byte first)
buf.putInt(0x01020304);
buf.flip();
System.out.printf("%02X %02X %02X %02X%n",
buf.get(), buf.get(), buf.get(), buf.get()); // 01 02 03 04
// Switch to LITTLE_ENDIAN (least significant byte first)
buf.clear();
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.putInt(0x01020304);
buf.flip();
System.out.printf("%02X %02X %02X %02X%n",
buf.get(), buf.get(), buf.get(), buf.get()); // 04 03 02 01
// Practical: reading a binary file with mixed endianness
ByteBuffer header = ByteBuffer.allocate(16);
header.order(ByteOrder.LITTLE_ENDIAN); // most binary formats use LE
// ... read from channel ...
int magic = header.getInt(); // e.g., ELF magic
short type = header.getShort();
short arch = header.getShort();
Reading Files with FileChannel
FileChannelByteBufferchannel.read(buf)-1flip()clear()MappedByteBufferchannel.map()
import java.nio.channels.FileChannel;
import java.nio.file.*;
// Reading a binary file with FileChannel + ByteBuffer
Path path = Path.of("data.bin");
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(4096);
while (channel.read(buf) != -1) {
buf.flip();
while (buf.hasRemaining()) {
byte b = buf.get();
// process byte
}
buf.clear();
}
}
// Writing with FileChannel
try (FileChannel out = FileChannel.open(
Path.of("output.bin"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
ByteBuffer buf = ByteBuffer.allocate(8);
buf.putInt(42);
buf.putInt(100);
buf.flip();
out.write(buf); // writes exactly the bytes between position and limit
}
// Memory-mapped file β entire file mapped into virtual address space
try (FileChannel channel = FileChannel.open(path)) {
MappedByteBuffer mapped = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size());
// Read directly as if it were a ByteBuffer β OS handles paging
int firstInt = mapped.getInt(0);
}
MappedByteBufferDirect vs Heap Buffers
- Heap buffer (
): backed by aByteBuffer.allocate(n) on the JVM heap. Cheap to allocate, subject to GC.byte[] - Direct buffer (
): backed by native memory outside the JVM heap. More expensive to allocate, but I/O operations skip an extra copy when working with native I/O (OS can DMA directly).ByteBuffer.allocateDirect(n)
// Direct buffer β allocate once and reuse for high-throughput I/O
ByteBuffer directBuf = ByteBuffer.allocateDirect(64 * 1024); // 64 KB
System.out.println(directBuf.isDirect()); // true
// Heap buffer β good for in-memory processing
ByteBuffer heapBuf = ByteBuffer.allocate(1024);
System.out.println(heapBuf.isDirect()); // false
// When to use which:
// - DirectByteBuffer: repeated I/O operations, network buffers, file channels
// - HeapByteBuffer: in-memory packet parsing, message encoding/decoding,
// short-lived buffers, when allocation cost matters
// CAUTION: DirectByteBuffer is NOT freed by GC automatically β
// it uses a Cleaner internally, but pressure-based freeing may lag.
// For explicit control, use Java 9+ MemorySegment (Foreign Memory API).
ByteBuffer.allocateDirect()