ByteBuffer
8 mins
The ByteBuffer class is the most commonly used buffer in Java NIO, providing byte-level I/O operations with support for direct memory allocation, byte order management, and view operations for other primitive types.
Source Code #
Core Implementation #
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer> {
// Factory methods
public static ByteBuffer allocate(int capacity) {
return new HeapByteBuffer(capacity, capacity);
}
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
public static ByteBuffer wrap(byte[] array) {
return new HeapByteBuffer(array, 0, array.length);
}
public static ByteBuffer wrap(byte[] array, int offset, int length) {
return new HeapByteBuffer(array, offset, length);
}
// Byte order management
public final ByteOrder order() {
return ByteOrder.BIG_ENDIAN;
}
public final ByteBuffer order(ByteOrder bo) {
return this;
}
// View operations
public abstract CharBuffer asCharBuffer();
public abstract ShortBuffer asShortBuffer();
public abstract IntBuffer asIntBuffer();
public abstract LongBuffer asLongBuffer();
public abstract FloatBuffer asFloatBuffer();
public abstract DoubleBuffer asDoubleBuffer();
// Compact operation
public abstract ByteBuffer compact();
// Duplicate and slice
public abstract ByteBuffer duplicate();
public abstract ByteBuffer slice();
public abstract ByteBuffer slice(int index, int length);
// Absolute and relative get/put operations
public abstract byte get();
public abstract byte get(int index);
public abstract ByteBuffer put(byte b);
public abstract ByteBuffer put(int index, byte b);
}
Implementation Details #
Byte Order Management #
ByteBuffer supports both big-endian and little-endian byte ordering:
// Get current byte order
ByteOrder order = buffer.order();
// Set byte order (affects multi-byte operations)
buffer.order(ByteOrder.LITTLE_ENDIAN);
// Byte order affects:
// - asCharBuffer(), asShortBuffer(), asIntBuffer(), etc.
// - getChar(), putChar(), getInt(), putInt(), etc.
// - View buffer operations
View Operations #
ByteBuffer can be viewed as buffers of other primitive types:
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// Create view buffers (share underlying data)
CharBuffer charBuffer = byteBuffer.asCharBuffer();
IntBuffer intBuffer = byteBuffer.asIntBuffer();
LongBuffer longBuffer = byteBuffer.asLongBuffer();
FloatBuffer floatBuffer = byteBuffer.asFloatBuffer();
DoubleBuffer doubleBuffer = byteBuffer.asDoubleBuffer();
ShortBuffer shortBuffer = byteBuffer.asShortBuffer();
// Views maintain their own position, limit, and mark
// Changes to view buffers affect the underlying ByteBuffer
Compact Operation #
The compact() method moves remaining data to the beginning of the buffer:
public ByteBuffer compact() {
int pos = position();
int lim = limit();
int rem = (pos <= lim ? lim - pos : 0);
// Copy remaining bytes to beginning
for (int i = 0; i < rem; i++) {
put(i, get(pos + i));
}
position(rem);
limit(capacity());
discardMark();
return this;
}
// Usage pattern for write-read-compact cycle
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Write data...
buffer.flip(); // Prepare for reading
// Read some data...
buffer.compact(); // Move unread data to beginning, prepare for writing
Duplicate and Slice Operations #
// Duplicate: Independent buffer with shared content
ByteBuffer original = ByteBuffer.allocate(1024);
ByteBuffer duplicate = original.duplicate();
// duplicate has independent position/limit/mark
// but shares underlying data array
// Slice: Buffer that shares a subsequence
original.position(256).limit(768);
ByteBuffer slice = original.slice();
// slice represents bytes 256-767 of original
// slice.capacity() = 512 (768-256)
// slice.position() = 0, slice.limit() = 512
Bulk Operations #
// Bulk get operations
byte[] dest = new byte[100];
buffer.get(dest); // Fill entire array
buffer.get(dest, 0, 50); // Fill first 50 elements
// Bulk put operations
byte[] src = new byte[100];
buffer.put(src); // Put entire array
buffer.put(src, 0, 50); // Put first 50 elements
// Transfer between buffers
ByteBuffer srcBuffer = ByteBuffer.allocate(100);
ByteBuffer dstBuffer = ByteBuffer.allocate(200);
srcBuffer.flip();
dstBuffer.put(srcBuffer); // Transfer remaining bytes
Direct vs Heap Allocation #
// Heap buffer (allocated in JVM heap)
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
// Characteristics:
// - Backed by byte[] array
// - Subject to garbage collection
// - Faster allocation
// - Slower I/O (requires copying)
// Direct buffer (allocated in native memory)
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
// Characteristics:
// - Backed by native memory
// - Not subject to garbage collection (uses Cleaner)
// - Slower allocation
// - Faster I/O (zero-copy possible)
// - Requires explicit cleanup
Performance Characteristics #
| Operation | Heap Buffer | Direct Buffer |
|---|---|---|
| Allocation | O(1) fast | O(n) slower (native malloc) |
| I/O Operations | Slower (copy) | Faster (zero-copy) |
| GC Pressure | High | None (native memory) |
| Memory Overhead | Low (byte[] + object) | High (native + Cleaner) |
| Access Patterns | Random access fast | Random access slower (JNI) |
Common Patterns #
Read-Process-Write with Compact #
ByteBuffer buffer = ByteBuffer.allocate(8192);
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
while (channel.read(buffer) != -1) {
buffer.flip(); // Prepare for reading
while (buffer.remaining() >= 1024) {
// Process 1KB chunks
processChunk(buffer);
}
buffer.compact(); // Move remaining data to beginning
}
// Handle any remaining data
buffer.flip();
if (buffer.hasRemaining()) {
processRemaining(buffer);
}
Scattering Reads with ByteBuffer Array #
ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(10240);
ByteBuffer[] buffers = {header, body};
// Read into multiple buffers
channel.read(buffers);
// Process each buffer
for (ByteBuffer buf : buffers) {
buf.flip();
// Process buffer contents
}
Gathering Writes #
ByteBuffer header = ByteBuffer.wrap("HEADER".getBytes());
ByteBuffer body = ByteBuffer.wrap(data);
ByteBuffer footer = ByteBuffer.wrap("FOOTER".getBytes());
ByteBuffer[] buffers = {header, body, footer};
// Write from multiple buffers in single operation
channel.write(buffers);
Memory-Mapped File Operations #
FileChannel channel = FileChannel.open(path,
StandardOpenOption.READ, StandardOpenOption.WRITE);
// Map file into memory
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, channel.size());
// Access as ByteBuffer
mappedBuffer.put(0, (byte)42);
mappedBuffer.putInt(100, 123456);
// Changes are flushed to disk eventually
// or call mappedBuffer.force() for immediate sync
ByteBuffer Views for Multi-byte Data #
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.order(ByteOrder.LITTLE_ENDIAN);
// Write multi-byte values
buffer.putInt(0x12345678);
buffer.putLong(0x1234567890ABCDEFL);
buffer.putDouble(3.14159);
buffer.putChar('A');
// Read back
buffer.flip();
int intValue = buffer.getInt();
long longValue = buffer.getLong();
double doubleValue = buffer.getDouble();
char charValue = buffer.getChar();
// Using view buffers
buffer.clear();
IntBuffer intView = buffer.asIntBuffer();
intView.put(0, 42);
intView.put(1, 100);
// Original buffer now contains the bytes
Best Practices #
Choose Allocation Type Wisely:
- Use heap buffers for short-lived, small buffers
- Use direct buffers for long-lived, large buffers (>64KB) with frequent I/O
- Consider memory-mapped files for random access to large files
Reuse Buffers:
// Buffer pool for direct buffers BufferPool pool = new BufferPool(10, 8192); ByteBuffer buffer = pool.acquire(); try { // Use buffer... } finally { pool.release(buffer); }Handle Byte Order:
- Always set byte order explicitly when working with multi-byte data
- Default is BIG_ENDIAN (network byte order)
- Use LITTLE_ENDIAN for performance on x86 architectures
Use Bulk Operations:
- Prefer bulk
get(byte[])andput(byte[])over single-byte operations - Use
transferToandtransferFromfor buffer-to-buffer copies
- Prefer bulk
Clean Up Direct Buffers:
// Direct buffers should be explicitly cleared ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024); // Use try-with-resources pattern if implementing AutoCloseable // or rely on Cleaner mechanism
Common Pitfalls #
- Forgetting to Flip: Reading from a buffer without calling
flip()after writing - Byte Order Mismatch: Reading multi-byte data with different byte order than written
- Buffer Underflow: Attempting to read more data than remaining without checking
- Memory Leaks: Not releasing direct buffers (rely on Cleaner but monitor native memory)
- Thread Safety: ByteBuffer is not thread-safe for concurrent access
- View Buffer Position: View buffers have independent position/limit from parent ByteBuffer
- Compact Side Effects:
compact()modifies buffer state (position = remaining, limit = capacity)
Internal Implementation #
HeapByteBuffer #
class HeapByteBuffer extends ByteBuffer {
final byte[] hb; // The underlying byte array
final int offset; // The offset into the array
HeapByteBuffer(int cap, int lim) {
super(-1, 0, lim, cap, null);
hb = new byte[cap];
offset = 0;
}
public byte get() {
return hb[ix(nextGetIndex())];
}
public ByteBuffer put(byte x) {
hb[ix(nextPutIndex())] = x;
return this;
}
int ix(int i) {
return i + offset;
}
}
DirectByteBuffer #
class DirectByteBuffer extends ByteBuffer {
private final long address; // Native memory address
private final Cleaner cleaner; // For freeing native memory
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap, null);
address = UNSAFE.allocateMemory(cap);
cleaner = Cleaner.create(this, new Deallocator(address));
}
public byte get() {
return UNSAFE.getByte(address + ix(nextGetIndex()));
}
public ByteBuffer put(byte x) {
UNSAFE.putByte(address + ix(nextPutIndex()), x);
return this;
}
private static class Deallocator implements Runnable {
private final long address;
Deallocator(long address) { this.address = address; }
public void run() {
UNSAFE.freeMemory(address);
}
}
}
ByteBufferAsXBuffer Views #
// Generated view classes for type conversions
class ByteBufferAsCharBufferB extends CharBuffer {
private final ByteBuffer bb;
private final int offset;
ByteBufferAsCharBufferB(ByteBuffer bb) {
super(-1, 0, bb.remaining() >> 1, bb.remaining() >> 1);
this.bb = bb.duplicate().order(ByteOrder.BIG_ENDIAN);
this.offset = bb.position();
}
public char get() {
return bb.getChar(ix(nextGetIndex()));
}
public CharBuffer put(char x) {
bb.putChar(ix(nextPutIndex()), x);
return this;
}
int ix(int i) {
return (i << 1) + offset;
}
}
Performance Optimization #
Buffer Pooling #
public class ByteBufferPool {
private final BlockingQueue<ByteBuffer> pool;
public ByteBufferPool(int size, int bufferSize) {
pool = new ArrayBlockingQueue<>(size);
for (int i = 0; i < size; i++) {
pool.add(ByteBuffer.allocateDirect(bufferSize));
}
}
public ByteBuffer acquire() throws InterruptedException {
ByteBuffer buffer = pool.take();
buffer.clear();
return buffer;
}
public void release(ByteBuffer buffer) {
buffer.clear();
pool.put(buffer);
}
}
Zero-Copy File Transfer #
// Memory-mapped file for zero-copy access
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size());
// Direct buffer to socket (zero-copy)
SocketChannel socketChannel = SocketChannel.open();
socketChannel.write(buffer);
Composite Buffers #
// Combine multiple buffers for single I/O operation
ByteBuffer[] buffers = new ByteBuffer[] {
ByteBuffer.wrap(headerBytes),
dataBuffer,
ByteBuffer.wrap(footerBytes)
};
// Single write operation for all buffers
channel.write(buffers);
Related Classes #
- CharBuffer: Character buffer for text processing
- IntBuffer: Integer buffer implementation
- LongBuffer: Long buffer implementation
- FloatBuffer: Float buffer implementation
- DoubleBuffer: Double buffer implementation
- ShortBuffer: Short buffer implementation
- MappedByteBuffer: Memory-mapped file buffer
- ByteOrder: Byte order specification