JDK 26 Introduces ofFileChannel: Simplify Large File Chunk Uploads
JDK 26 adds HttpRequest.BodyPublishers.ofFileChannel, letting developers read specific file segments directly from a shared FileChannel without copying data into heap memory, dramatically reducing memory usage and simplifying parallel chunk uploads to object storage such as OSS or S3.
When uploading multi‑gigabyte videos to object storage (e.g., Alibaba Cloud OSS), the typical approach is to split the file into parts, upload each part in parallel, and let the server reassemble them. Using JDK 17’s standard HttpClient, the only available API was HttpRequest.BodyPublishers.ofFile(Path), which uploads the entire file.
Why the old method is cumbersome
To upload a single part, developers had to manually open a RandomAccessFile, seek to the part’s offset, read the bytes into a byte[], wrap that array in a BodyPublisher, and repeat this for every part. This leads to a large amount of boilerplate code and, under high concurrency, many buffers occupy heap memory simultaneously, putting pressure on the JVM.
// Old way: manually read a specific range
long offset = partIndex * partSize;
byte[] buffer = new byte[(int) partSize];
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
raf.seek(offset);
int bytesRead = raf.read(buffer);
HttpRequest.BodyPublisher publisher =
HttpRequest.BodyPublishers.ofByteArray(buffer, 0, bytesRead);
// build and send request …
}Each part allocation creates a byte[] on the heap; uploading ten 10 MB parts simultaneously consumes roughly 100 MB of heap space, and the problem worsens with larger files.
What JDK 26 adds
JDK 26 introduces a new method on HttpRequest.BodyPublishers:
HttpRequest.BodyPublishers.ofFileChannel(FileChannel chan, long position, long size) chan: an already opened
FileChannel position: the start byte offset in the file size: the number of bytes to read
This method reads the specified range directly from the FileChannel and streams it as the HTTP request body without ever copying the data into heap memory.
How it works internally
The implementation uses the FileChannel ’s ability to perform zero‑copy transfers via DirectByteBuffer. Data moves from the kernel page cache straight to the network stack, bypassing the Java heap and eliminating the intermediate byte[] allocation.
A quoted issue (JDK‑8329829) tracks this addition, released with JDK 26 on 2026‑03‑17.
Thread‑safety and design rationale
The API specifies the position argument explicitly, leaving the channel’s own position unchanged. Consequently, multiple threads can share a single FileChannel and invoke ofFileChannel concurrently on different ranges safely.
OpenJDK developers considered an alternative signature ofFile(Path, long position, long size) that would open a new FileChannel per call. They rejected it for two reasons:
Resource lifecycle control : callers would lose visibility over when the channel is opened and closed.
High‑concurrency resource reuse : opening a channel per request would exhaust file descriptors when dozens or hundreds of parts are uploaded simultaneously. Sharing one channel reduces descriptor usage and avoids repeated open/close overhead.
Practical example
The following demo shows a parallel chunk uploader that opens a single FileChannel and uses the new API for each part:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class ParallelChunkUploader {
private static final long PART_SIZE = 10 * 1024 * 1024L; // 10 MB per part
private static final String UPLOAD_URL = "https://your-storage.example.com/upload/part";
public static void main(String[] args) throws Exception {
Path filePath = Path.of("large-video.mp4");
try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ);
HttpClient client = HttpClient.newHttpClient()) {
long fileSize = channel.size();
long totalParts = (fileSize + PART_SIZE - 1) / PART_SIZE;
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (int i = 0; i < totalParts; i++) {
final int partIndex = i;
final long offset = (long) i * PART_SIZE;
final long length = Math.min(PART_SIZE, fileSize - offset);
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
var publisher = HttpRequest.BodyPublishers.ofFileChannel(channel, offset, length);
var request = HttpRequest.newBuilder()
.uri(URI.create(UPLOAD_URL + "?partNumber=" + partIndex))
.header("Content-Type", "application/octet-stream")
.POST(publisher)
.build();
client.send(request, HttpResponse.BodyHandlers.discarding());
System.out.println("Part " + partIndex + " uploaded");
} catch (Exception e) {
throw new RuntimeException("Part " + partIndex + " upload failed", e);
}
});
futures.add(future);
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println("All parts uploaded, total " + totalParts + " parts");
}
}
}The key line is the creation of the publisher via ofFileChannel(channel, offset, length), which avoids any extra heap allocation.
ofFileChannel reads from the specified position without altering the channel’s own position, so concurrent calls on the same channel are safe.
Why zero‑copy matters
Traditional InputStream reads involve two copies: kernel page cache → Java heap → network stack. Each part therefore creates a byte[] that adds GC pressure. By using FileChannel with a DirectByteBuffer, data can be transferred directly from kernel space to the network stack, a technique known as zero‑copy.
Typical scenarios that benefit
Uploading multi‑gigabyte objects to OSS/S3/MinIO – memory usage stays low even with dozens of concurrent parts.
Implementing resumable uploads – the caller can record the last successful part index and resume from the corresponding offset.
Reading multiple disjoint ranges from the same file concurrently – the channel’s position is unchanged, so no extra synchronization is required.
Additional JDK 26 fix
JDK 26 also corrects the exception type thrown by BodyPublishers.ofFile(Path) when the path is not on the default file system. Previously it threw NoSuchFileException; the API now correctly throws FileNotFoundException.
Conclusion
The new ofFileChannel method fills a long‑standing gap in the Java HTTP client, allowing efficient, low‑memory, parallel chunk uploads without custom buffering logic. While the author has not yet deployed it in production (still on JDK 21 LTS), the demo shows noticeably fewer lines of code and reduced heap pressure.
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.
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.
