Java NIO: Channels & Buffers

Java NIO (New I/O, introduced in Java 1.4) provides channels and buffers as an alternative to the classic stream-based I/O. Channels represent open connections to I/O devices (files, sockets). Buffers are fixed-size memory blocks that data passes through. The combination enables high-throughput file operations via FileChannel, and non-blocking network I/O via SocketChannel and Selector.

Contents

ByteBuffer Basics

A ByteBuffer tracks three positions: capacity (total size), limit (last valid byte), and position (next read/write index). Call flip() before reading data that was just written.

Java
import java.nio.ByteBuffer;

// Allocate a heap buffer
ByteBuffer buf = ByteBuffer.allocate(1024);

// Write bytes
buf.put("Hello NIO".getBytes());

// Flip: limit = current position, position = 0
buf.flip();

// Read bytes
byte[] bytes = new byte[buf.limit()];
buf.get(bytes);
System.out.println(new String(bytes)); // Hello NIO

// Rewind: position = 0, limit unchanged (re-read)
buf.rewind();

// Clear: position = 0, limit = capacity (ready to write again)
buf.clear();

// Direct buffer — allocated outside the Java heap (faster for I/O)
ByteBuffer direct = ByteBuffer.allocateDirect(4096);
Direct buffers (ByteBuffer.allocateDirect()) avoid an extra copy between kernel space and JVM heap during I/O, but they are slower to allocate and GC'd differently. Reuse them for large, long-lived I/O operations.

FileChannel: Reading & Writing

Open a FileChannel from a RandomAccessFile, FileInputStream, or via FileChannel.open(). All reads and writes go through ByteBuffer.

Java
import java.nio.channels.FileChannel;
import java.nio.file.*;

// Writing to a file
Path path = Path.of("output.bin");
try (FileChannel fc = FileChannel.open(path,
        StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {

    ByteBuffer buf = ByteBuffer.allocate(64);
    buf.putInt(42);
    buf.putDouble(3.14);
    buf.flip();           // prepare buffer for reading by the channel
    fc.write(buf);
}

// Reading from a file
try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ)) {

    ByteBuffer buf = ByteBuffer.allocate(64);
    int bytesRead = fc.read(buf); // fills buffer; returns -1 at EOF
    buf.flip();                   // prepare buffer for reading by our code

    int   intVal    = buf.getInt();
    double dblVal   = buf.getDouble();
    System.out.printf("int=%d  double=%.2f%n", intVal, dblVal);
}

FileChannel.transferTo / transferFrom

transferTo() and transferFrom() copy bytes directly between channels using OS-level zero-copy when possible: significantly faster than copying through a buffer.

Java
Path src  = Path.of("source.dat");
Path dest = Path.of("destination.dat");

try (FileChannel from = FileChannel.open(src,  StandardOpenOption.READ);
     FileChannel to   = FileChannel.open(dest, StandardOpenOption.CREATE,
                                               StandardOpenOption.WRITE)) {

    long size = from.size();
    long transferred = 0;
    while (transferred < size) {
        transferred += from.transferTo(transferred, size - transferred, to);
    }
    System.out.println("Copied " + transferred + " bytes");
}
On Linux, transferTo() uses the sendfile() system call which copies data from the kernel read buffer directly to the socket/file without passing through user space: this is the mechanism behind HTTP file serving and Kafka's high throughput.

Scatter / Gather I/O

Scatter reads fill multiple buffers in one system call; gather writes drain multiple buffers in one call. Useful for reading structured messages with distinct header and body regions.

Java
// Scatter read — fill header buffer then body buffer
ByteBuffer header = ByteBuffer.allocate(16);
ByteBuffer body   = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = { header, body };

try (FileChannel fc = FileChannel.open(Path.of("message.bin"), StandardOpenOption.READ)) {
    long bytesRead = fc.read(buffers);  // scatter: fills header first, then body
    header.flip();
    body.flip();

    int msgType   = header.getInt();
    int bodyLen   = header.getInt();
    // … read body bytes …
}

// Gather write — write header + body in one call
header.clear();
header.putInt(1);   // message type
header.putInt(64);  // body length
header.flip();

body.clear();
body.put("payload data".getBytes());
body.flip();

try (FileChannel fc = FileChannel.open(Path.of("out.bin"),
        StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
    fc.write(buffers);  // gather: writes header then body
}

SocketChannel: Non-blocking Client

Put a SocketChannel into non-blocking mode to connect and communicate without tying up a thread.

Java
import java.nio.channels.SocketChannel;
import java.net.InetSocketAddress;

SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);

boolean connected = channel.connect(new InetSocketAddress("example.com", 80));

// In non-blocking mode connect() may return false — finish via finishConnect()
while (!channel.finishConnect()) {
    // do other work or spin
}

// Send an HTTP request
String request = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
ByteBuffer buf = ByteBuffer.wrap(request.getBytes());
channel.write(buf);

// Read response
ByteBuffer response = ByteBuffer.allocate(4096);
int read;
StringBuilder sb = new StringBuilder();
while ((read = channel.read(response)) > 0) {
    response.flip();
    sb.append(new String(response.array(), 0, read));
    response.clear();
}
System.out.println(sb);
channel.close();

ServerSocketChannel & Selector

A Selector monitors multiple channels in a single thread. Register channels with the selector and call select() to get a set of channels that are ready for I/O.

Java
import java.nio.channels.*;
import java.net.InetSocketAddress;
import java.util.Iterator;

ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(9090));

Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("Server listening on port 9090");

while (true) {
    selector.select();  // blocks until at least one channel is ready

    Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
    while (keys.hasNext()) {
        SelectionKey key = keys.next();
        keys.remove();

        if (key.isAcceptable()) {
            // Accept new connection
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);

        } else if (key.isReadable()) {
            // Read from client
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buf = ByteBuffer.allocate(256);
            int bytesRead = client.read(buf);
            if (bytesRead == -1) {
                client.close();
            } else {
                buf.flip();
                client.write(buf); // echo back
            }
        }
    }
}
This single-thread event loop can handle thousands of concurrent connections, which is the pattern used by Netty and other high-performance networking frameworks under the hood.

Memory-Mapped Files

Map a file (or a region of it) directly into the JVM's address space. Access is then as fast as reading an in-memory array: the OS handles loading pages from disk on demand.

Java
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;

Path path = Path.of("large-data.bin");

try (FileChannel fc = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE)) {

    // Map the entire file into memory
    MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size());

    // Random-access reads and writes — no system call per access
    mbb.position(1024);
    int value = mbb.getInt();
    mbb.position(2048);
    mbb.putInt(value * 2);

    // Force pending changes to disk (optional — OS flushes eventually)
    mbb.force();
}
Memory-mapped files are limited to 2 GB regions on 32-bit JVMs. On 64-bit JVMs you can map files larger than available RAM because the OS uses demand paging, but mapping and unmapping large regions has overhead and can cause GC pressure.