Skip to main content
  1. Java NIO (New I/O)/

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 #

View Source on GitHub

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 #

OperationHeap BufferDirect Buffer
AllocationO(1) fastO(n) slower (native malloc)
I/O OperationsSlower (copy)Faster (zero-copy)
GC PressureHighNone (native memory)
Memory OverheadLow (byte[] + object)High (native + Cleaner)
Access PatternsRandom access fastRandom 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 #

  1. 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
  2. Reuse Buffers:

    // Buffer pool for direct buffers
    BufferPool pool = new BufferPool(10, 8192);
    ByteBuffer buffer = pool.acquire();
    try {
        // Use buffer...
    } finally {
        pool.release(buffer);
    }
    
  3. 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
  4. Use Bulk Operations:

    • Prefer bulk get(byte[]) and put(byte[]) over single-byte operations
    • Use transferTo and transferFrom for buffer-to-buffer copies
  5. 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 #

  1. Forgetting to Flip: Reading from a buffer without calling flip() after writing
  2. Byte Order Mismatch: Reading multi-byte data with different byte order than written
  3. Buffer Underflow: Attempting to read more data than remaining without checking
  4. Memory Leaks: Not releasing direct buffers (rely on Cleaner but monitor native memory)
  5. Thread Safety: ByteBuffer is not thread-safe for concurrent access
  6. View Buffer Position: View buffers have independent position/limit from parent ByteBuffer
  7. 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);