Unlocking High-Performance I/O: Java Zero-Copy Techniques Explained
This article explains the principles of zero‑copy I/O, covering buffer concepts, virtual memory, mmap + write and sendfile methods, and demonstrates Java implementations using MappedByteBuffer, DirectByteBuffer, channel‑to‑channel transfers, as well as Netty’s CompositeChannelBuffer for efficient data handling.
Preface
Zero‑copy means data does not need to be copied back and forth, greatly improving system performance. The term appears frequently in Java NIO, Netty, Kafka, RocketMQ and similar frameworks as a key performance highlight. Below we start with basic I/O concepts and then analyse zero‑copy.
I/O Concepts
1. Buffer
The buffer is the foundation of all I/O. I/O essentially moves data into or out of a buffer. When a process performs an I/O operation, it requests the OS to either drain the buffer (write) or fill it (read). The following diagram shows a typical Java read request flow:
After the kernel receives a read request, it first checks whether the required data already resides in kernel space. If so, it copies the data directly to the process’s buffer; otherwise the kernel commands the disk controller to read the data, which is written directly into the kernel read buffer via DMA.
The kernel then copies the data to the process buffer. For a write request, the kernel copies data from the user buffer to the kernel socket buffer, and DMA transfers it to the NIC for transmission.
Because each copy consumes resources, zero‑copy was introduced to eliminate these copies. Two common zero‑copy methods are mmap+write and sendfile.
2. Virtual Memory
All modern operating systems use virtual memory, mapping virtual addresses to physical ones. Benefits include:
Multiple virtual addresses can refer to the same physical memory.
Virtual address space can be larger than physical memory.
Using the first property, kernel space addresses and user‑space virtual addresses can be mapped to the same physical address, allowing DMA to fill a buffer visible to both kernel and user space, as illustrated below:
This eliminates copies between kernel and user space; Java leverages this OS feature for performance. The following sections detail Java’s zero‑copy support.
3. mmap+write Method
The mmap+write approach replaces the traditional read+write sequence. mmap maps a file (or other object) into a process’s address space, establishing a one‑to‑one relationship between a file’s disk address and a range of virtual addresses.
This removes the need to copy data from the kernel read buffer to the user buffer, though a copy from the kernel read buffer to the kernel socket buffer is still required, as shown:
4. sendfile Method
The sendfile system call, introduced in kernel 2.1, simplifies data transfer between two channels over the network. It reduces data copying and context switches, as illustrated:
Data transmission occurs entirely in kernel space, eliminating one context switch. Linux 2.4 further improved this by recording the data’s memory address and offset in the socket buffer, removing the remaining kernel‑space copy.
Java Zero-Copy
1. MappedByteBuffer
Java NIO’s FileChannel provides a map() method that creates a virtual memory mapping between an opened file and a MappedByteBuffer. MappedByteBuffer extends ByteBuffer, but its data resides in a file on disk.
Calling get() reads data from disk, reflecting the file’s current content; put() updates the file, and changes are visible to other readers. Example:
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
File file = new File("D://db.txt");
long len = file.length();
byte[] ds = new byte[(int) len];
MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, len);
for (int offset = 0; offset < len; offset++) {
byte b = mappedByteBuffer.get();
ds[offset] = b;
}
Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
while (scan.hasNext()) {
System.out.print(scan.next() + " ");
}
}
}The map() method signature:
public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;Parameters:
mode : Mapping mode (READ_ONLY, READ_WRITE, PRIVATE).
position : Starting offset in the file.
size : Number of bytes to map.
In PRIVATE mode, modifications create a private copy visible only to the buffer; the underlying file remains unchanged.
Internally, the native method obtains the mapping address; if it fails, the JVM triggers GC and retries. The resulting object is actually a DirectByteBuffer.
2. DirectByteBuffer
DirectByteBufferextends MappedByteBuffer and allocates memory outside the JVM heap, avoiding heap pressure. It can be created via FileChannel.map() or manually:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);This allocates 100 bytes of off‑heap memory.
3. Channel‑to‑Channel Transfer
FileChannel’s transferTo() method moves data directly between channels without an intermediate user‑space buffer. Example:
public class ChannelTransfer {
public static void main(String[] argv) throws Exception {
String[] files = new String[1];
files[0] = "D://db.txt";
catFiles(Channels.newChannel(System.out), files);
}
private static void catFiles(WritableByteChannel target, String[] files) throws Exception {
for (int i = 0; i < files.length; i++) {
FileInputStream fis = new FileInputStream(files[i]);
FileChannel channel = fis.getChannel();
channel.transferTo(0, channel.size(), target);
channel.close();
fis.close();
}
}
}Signature:
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;Parameters are the start position, number of bytes, and the destination channel. This avoids copying data into a user‑space buffer; both source and destination maintain their own kernel buffers.
Netty Zero-Copy
Netty provides zero‑copy buffers. When transmitting data, Netty needs to assemble and split messages; the native NIO ByteBuffer cannot do this efficiently. Netty uses CompositeChannelBuffer (for aggregation) and Slice (for splitting) to achieve zero‑copy.
In Netty, TCP‑level HTTP packets are split into two ChannelBuffer objects that are meaningless to the application layer. By combining them into a CompositeChannelBuffer, they form a coherent HTTP message, referred to as a “Virtual Buffer”. Example source snippet:
public class CompositeChannelBuffer extends AbstractChannelBuffer {
private final ByteOrder order;
private ChannelBuffer[] components;
private int[] indices;
private int lastAccessedComponentId;
private final boolean gathering;
public byte getByte(int index) {
int componentId = componentId(index);
return components[componentId].getByte(index - indices[componentId]);
}
// ...
}The buffer stores references to all received buffers; no new memory is allocated, and reads/writes operate directly on the component buffers, achieving zero‑copy.
Other Zero‑Copy Uses
RocketMQ writes messages sequentially to a commit‑log file and uses a consume‑queue as an index; it employs mmap+write zero‑copy to serve consumer requests. Kafka also uses the sendfile zero‑copy technique for persisting network data to disk and sending files over the network.
Summary
Zero‑copy can be understood as operating on object references rather than duplicating objects; there is only a single instance of the data, and any modification through a reference is reflected everywhere.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
