The full picture
// Naive proxy loop — 4 copies, 4 user/kernel switches per chunk:
while ((n = fileIn.read(buf)) > 0) // DMA->pageCache, pageCache->buf
socketOut.write(buf, 0, n); // buf->socketBuf, socketBuf->NIC
// Zero-copy — CPU copies: 0 (with SG-DMA), switches: 2:
FileChannel file = FileChannel.open(path);
long sent = file.transferTo(pos, count, socketChannel); // sendfile(2)
// The catch that defines its use-case: the kernel moves bytes it never
// shows you — no transformation possible. TLS breaks it (must encrypt)
// unless the NIC does kTLS offload; Kafka with TLS loses sendfile.- Kafka's full recipe (worth reciting as one breath): append-only sequential writes → OS writeback batches them; consumers read recent data → served from page cache; transport = sendfile → no JVM heap involvement, no GC pressure from payload bytes; ordering by offset → readahead wins. 'Kafka is fast because it cooperates with the OS instead of fighting it.'
- mmap flavor: map the file, write to memory, kernel handles paging — removes copies for random access patterns (index files, MappedByteBuffer) but page faults become your I/O and error handling gets weird (SIGBUS on truncation) — the trade vs transferTo.
- Limits: any byte transformation (compression, encryption, protocol framing) forces data through the CPU anyway — then the game becomes doing it once (kTLS, NIC offloads) or not at all (Kafka consumers decompress, broker doesn't).
- The pattern generalizes: splice/vmsplice, NGINX serving static files, gRPC's slice buffers, NVMe-oF — 'don't copy what you don't touch' is a design principle, not a syscall.