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

FileChannel

13 mins

The FileChannel class provides a channel for reading, writing, mapping, and manipulating files with support for random access, memory‑mapped I/O, file locking, and efficient transfer operations.

Source Code #

View Source on GitHub

Core Implementation #

FileChannel is an abstract class extending AbstractInterruptibleChannel and implementing SeekableByteChannel, GatheringByteChannel, and ScatteringByteChannel. The concrete implementation is FileChannelImpl in the sun.nio.ch package.

public abstract class FileChannel extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
    
    // Factory method to open a file channel
    public static FileChannel open(Path path, Set<? extends OpenOption> options,
                                   FileAttribute<?>... attrs) throws IOException {
        return FileSystemProvider.provider(path).newFileChannel(path, options, attrs);
    }
    
    // Reads a sequence of bytes from this channel into the given buffer
    public abstract int read(ByteBuffer dst) throws IOException;
    
    // Reads a sequence of bytes from this channel into a set of buffers
    public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
    
    // Writes a sequence of bytes to this channel from the given buffer
    public abstract int write(ByteBuffer src) throws IOException;
    
    // Writes a sequence of bytes to this channel from a set of buffers
    public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
    
    // Returns this channel's file position
    public abstract long position() throws IOException;
    
    // Sets this channel's file position
    public abstract FileChannel position(long newPosition) throws IOException;
    
    // Returns the current size of this channel's file
    public abstract long size() throws IOException;
    
    // Truncates this channel's file to the given size
    public abstract FileChannel truncate(long size) throws IOException;
    
    // Forces any updates to this channel's file to be written to the storage device
    public abstract void force(boolean metaData) throws IOException;
    
    // Transfers bytes from this channel's file to the given writable byte channel
    public abstract long transferTo(long position, long count,
                                    WritableByteChannel target) throws IOException;
    
    // Transfers bytes into this channel's file from the given readable byte channel
    public abstract long transferFrom(ReadableByteChannel src,
                                      long position, long count) throws IOException;
    
    // Maps a region of this channel's file directly into memory
    public abstract MappedByteBuffer map(MapMode mode, long position, long size)
        throws IOException;
    
    // Acquires a lock on the given region of this channel's file
    public abstract FileLock lock(long position, long size, boolean shared)
        throws IOException;
    
    // Attempts to acquire a lock on the given region of this channel's file
    public abstract FileLock tryLock(long position, long size, boolean shared)
        throws IOException;
    
    // Memory-mapping modes
    public static class MapMode {
        public static final MapMode READ_ONLY = new MapMode("READ_ONLY");
        public static final MapMode READ_WRITE = new MapMode("READ_WRITE");
        public static final MapMode PRIVATE = new MapMode("PRIVATE");
        private final String name;
        private MapMode(String name) { this.name = name; }
        public String toString() { return name; }
    }
}

FileChannelImpl Internal Structure #

The concrete implementation FileChannelImpl (in sun.nio.ch) contains these key fields:

private final FileDescriptor fd;          // Underlying OS file descriptor
private final boolean readable;           // Channel opened for reading
private final boolean writable;           // Channel opened for writing
private final boolean sync;               // O_SYNC / O_DSYNC flag
private final boolean direct;             // Direct I/O enabled
private final int alignment;              // Block size for Direct I/O
private final Object positionLock = new Object(); // Guards position-sensitive ops
private final NativeThreadSet threads;    // Tracks native threads blocked on I/O
private final FileLockTable fileLockTable; // Manages locks for this file
private final Cleanable closer;           // Clean-up action for the file descriptor

Opening File Channels #

File channels can be opened in several ways:

// Standard open with options
Path path = Paths.get("data.txt");
try (FileChannel channel = FileChannel.open(path,
        StandardOpenOption.READ,
        StandardOpenOption.WRITE,
        StandardOpenOption.CREATE)) {
    // Use channel
}

// From legacy FileInputStream/FileOutputStream/RandomAccessFile
FileInputStream fis = new FileInputStream("data.txt");
FileChannel readChannel = fis.getChannel();

FileOutputStream fos = new FileOutputStream("data.txt");
FileChannel writeChannel = fos.getChannel();

RandomAccessFile raf = new RandomAccessFile("data.txt", "rw");
FileChannel rwChannel = raf.getChannel();

Position Management and Random Access #

File channels maintain a current position that advances with each read/write operation, but also support absolute-position operations:

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    // Get current position
    long pos = channel.position();
    
    // Set position (relative)
    channel.position(pos + 100);
    
    // Read at absolute position (doesn't affect channel's position)
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead = channel.read(buffer, 2048); // Read from offset 2048
    
    // Write at absolute position
    buffer.flip();
    channel.write(buffer, 4096); // Write at offset 4096
    
    // Query file size
    long fileSize = channel.size();
    
    // Truncate file
    channel.truncate(1024); // Reduce file to 1KB
}

Memory-Mapped File I/O #

Memory mapping allows direct access to file data through a MappedByteBuffer:

// Map entire file for read-only access
try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    MappedByteBuffer mappedBuffer = channel.map(
        FileChannel.MapMode.READ_ONLY, 0, channel.size());
    
    // Access data directly through the buffer
    while (mappedBuffer.hasRemaining()) {
        byte b = mappedBuffer.get();
        // Process byte
    }
}

// Map region for read-write access
try (FileChannel channel = FileChannel.open(path,
        StandardOpenOption.READ,
        StandardOpenOption.WRITE)) {
    
    // Map first 8MB of file
    MappedByteBuffer mappedBuffer = channel.map(
        FileChannel.MapMode.READ_WRITE, 0, 8 * 1024 * 1024);
    
    // Modify data directly
    mappedBuffer.putInt(0, 42); // Write integer at offset 0
    
    // Changes are eventually written back to file
    // Force immediate write:
    mappedBuffer.force();
}

File Locking #

File channels support both shared (read) and exclusive (write) locks:

try (FileChannel channel = FileChannel.open(path,
        StandardOpenOption.READ,
        StandardOpenOption.WRITE)) {
    
    // Acquire exclusive lock on entire file (blocks until available)
    FileLock exclusiveLock = channel.lock();
    try {
        // Perform write operations
        channel.write(buffer);
    } finally {
        exclusiveLock.release();
    }
    
    // Acquire shared lock on region (non-blocking attempt)
    FileLock sharedLock = channel.tryLock(0, 1024, true); // Shared lock on first 1KB
    if (sharedLock != null) {
        try {
            // Perform read operations on locked region
            channel.read(buffer, 0);
        } finally {
            sharedLock.release();
        }
    }
    
    // Lock with timeout (implemented via tryLock with polling)
    FileLock lock = null;
    long timeoutMillis = 5000;
    long start = System.currentTimeMillis();
    while (lock == null && (System.currentTimeMillis() - start) < timeoutMillis) {
        lock = channel.tryLock(0, channel.size(), false);
        if (lock == null) {
            Thread.sleep(100);
        }
    }
    if (lock == null) {
        throw new IOException("Could not acquire lock within timeout");
    }
}

Efficient File Transfer #

The transferTo and transferFrom methods provide optimized data transfer between channels:

// Zero-copy file copy using transferTo
try (FileChannel source = FileChannel.open(sourcePath, StandardOpenOption.READ);
     FileChannel dest = FileChannel.open(destPath,
         StandardOpenOption.WRITE,
         StandardOpenOption.CREATE,
         StandardOpenOption.TRUNCATE_EXISTING)) {
    
    long position = 0;
    long count = source.size();
    
    while (position < count) {
        position += source.transferTo(position, count - position, dest);
    }
}

// Network file server: send file directly to socket
try (FileChannel fileChannel = FileChannel.open(filePath, StandardOpenOption.READ);
     SocketChannel socketChannel = SocketChannel.open(socketAddress)) {
    
    socketChannel.connect(socketAddress);
    
    // Transfer file directly to socket (may use sendfile/copy_file_range)
    long transferred = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
    System.out.println("Transferred " + transferred + " bytes");
}

// Receive data from network directly into file
try (FileChannel fileChannel = FileChannel.open(filePath,
         StandardOpenOption.WRITE,
         StandardOpenOption.CREATE);
     SocketChannel socketChannel = ServerSocketChannel.open().accept()) {
    
    long position = 0;
    ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
    
    while (true) {
        int read = socketChannel.read(buffer);
        if (read == -1) break;
        
        buffer.flip();
        position += fileChannel.write(buffer, position);
        buffer.clear();
    }
    
    // Alternative using transferFrom (less common for network)
    // fileChannel.transferFrom(socketChannel, 0, Long.MAX_VALUE);
}

Direct I/O Support #

When opened with StandardOpenOption.DIRECT, file channels perform I/O that bypasses the OS page cache:

Set<OpenOption> options = new HashSet<>();
options.add(StandardOpenOption.READ);
options.add(StandardOpenOption.DIRECT); // Enable Direct I/O

try (FileChannel channel = FileChannel.open(path, options)) {
    // Direct I/O requires alignment of position and buffer size
    // Misalignment throws IOException
    
    // Buffer size must be multiple of alignment
    int alignment = 4096; // Typical block size
    ByteBuffer buffer = ByteBuffer.allocateDirect(alignment);
    
    // Position must be aligned
    long position = 0;
    while (position < channel.size()) {
        int read = channel.read(buffer, position);
        if (read == -1) break;
        
        buffer.flip();
        // Process aligned block
        buffer.clear();
        position += alignment;
    }
}

Performance Characteristics #

OperationComplexityNotes
read() / write()O(1) per system callBuffer size affects throughput
read(ByteBuffer, long)O(1)Absolute position doesn’t affect performance
transferTo() / transferFrom()O(1) with zero-copyMay use sendfile, copy_file_range
map()O(1) setup, O(1) accessVirtual memory overhead for mapping
lock() / tryLock()O(1) to O(n)Depends on lock table implementation
force()O(1)May trigger synchronous write to disk

Key performance insights:

  • Buffer size matters: 8KB-64KB buffers typically optimal for sequential I/O
  • Direct buffers improve throughput for large transfers by avoiding copy
  • Memory mapping excels for random access patterns on large files
  • Transfer methods enable zero-copy when supported by OS and channel types
  • Direct I/O reduces cache pollution but requires careful alignment

Common Usage Patterns #

1. Sequential File Processing #

public static long processFileSequentially(Path path, ByteProcessor processor)
        throws IOException {
    long totalBytes = 0;
    try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(8192);
        
        while (channel.read(buffer) != -1) {
            buffer.flip();
            totalBytes += buffer.remaining();
            
            while (buffer.hasRemaining()) {
                processor.process(buffer.get());
            }
            
            buffer.clear();
        }
    }
    return totalBytes;
}

2. Random Access Database #

public class SimpleFileDatabase {
    private final FileChannel channel;
    private static final int RECORD_SIZE = 1024;
    
    public SimpleFileDatabase(Path path) throws IOException {
        channel = FileChannel.open(path,
            StandardOpenOption.READ,
            StandardOpenOption.WRITE,
            StandardOpenOption.CREATE);
    }
    
    public void writeRecord(int recordId, ByteBuffer data) throws IOException {
        if (data.remaining() > RECORD_SIZE) {
            throw new IllegalArgumentException("Record too large");
        }
        
        long position = recordId * (long) RECORD_SIZE;
        ByteBuffer record = ByteBuffer.allocate(RECORD_SIZE);
        record.put(data);
        record.flip();
        
        channel.write(record, position);
    }
    
    public ByteBuffer readRecord(int recordId) throws IOException {
        long position = recordId * (long) RECORD_SIZE;
        ByteBuffer buffer = ByteBuffer.allocate(RECORD_SIZE);
        
        int read = channel.read(buffer, position);
        if (read == -1) {
            return null; // Record doesn't exist
        }
        
        buffer.flip();
        return buffer;
    }
}

3. File Indexing with Memory Mapping #

public class MappedFileIndex {
    private MappedByteBuffer mappedBuffer;
    private final int indexOffset;
    
    public MappedFileIndex(Path filePath, long dataOffset) throws IOException {
        try (FileChannel channel = FileChannel.open(filePath,
                StandardOpenOption.READ,
                StandardOpenOption.WRITE)) {
            
            // Map the entire file
            mappedBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, channel.size());
            this.indexOffset = (int) dataOffset;
        }
    }
    
    public void putInt(int key, int value) {
        int position = indexOffset + (key * Integer.BYTES);
        mappedBuffer.putInt(position, value);
    }
    
    public int getInt(int key) {
        int position = indexOffset + (key * Integer.BYTES);
        return mappedBuffer.getInt(position);
    }
    
    public void sync() {
        mappedBuffer.force();
    }
}

Best Practices #

  1. Always Use Try-With-Resources:

    // Correct
    try (FileChannel channel = FileChannel.open(path, options)) {
        // Use channel
    }
    
    // Avoid
    FileChannel channel = FileChannel.open(path, options);
    try {
        // Use channel
    } finally {
        channel.close();
    }
    
  2. Choose Appropriate Buffer Types:

    // Direct buffers for large, repeated transfers
    ByteBuffer directBuffer = ByteBuffer.allocateDirect(8192);
    
    // Heap buffers for small, infrequent operations
    ByteBuffer heapBuffer = ByteBuffer.allocate(8192);
    
    // Mapped buffers for random access to large files
    MappedByteBuffer mappedBuffer = channel.map(MapMode.READ_ONLY, 0, fileSize);
    
  3. Monitor Transfer Performance:

    long startTime = System.nanoTime();
    long transferred = sourceChannel.transferTo(0, fileSize, destChannel);
    long elapsedNanos = System.nanoTime() - startTime;
    
    double throughput = (transferred / (elapsedNanos / 1_000_000_000.0)) / (1024 * 1024);
    System.out.printf("Transfer rate: %.2f MB/s%n", throughput);
    
  4. Handle Partial Transfers:

    // transferTo may not transfer all requested bytes
    long position = 0;
    long remaining = fileSize;
    
    while (remaining > 0) {
        long transferred = channel.transferTo(position, remaining, target);
        if (transferred <= 0) {
            throw new IOException("Transfer failed or made no progress");
        }
        position += transferred;
        remaining -= transferred;
    }
    
  5. Proper Lock Management:

    FileLock lock = null;
    try {
        lock = channel.lock(); // or tryLock()
        // Perform operations
    } catch (OverlappingFileLockException e) {
        // Handle lock conflict within same JVM
    } finally {
        if (lock != null) {
            lock.release();
        }
    }
    

Common Pitfalls #

  1. Forgetting to Flip Buffers: Reading from a buffer after writing without calling flip().
  2. Ignoring Return Values: Assuming read() or write() processes all requested bytes.
  3. Resource Leaks: Not closing channels in finally blocks or exception handlers.
  4. Lock Deadlocks: Acquiring locks in inconsistent order across threads/processes.
  5. Alignment Issues with Direct I/O: Not aligning position and buffer size when using DIRECT option.
  6. Memory Mapping Overhead: Mapping small files or many regions causing virtual memory fragmentation.
  7. Transfer Method Limitations: Assuming transferTo works with all channel types (some require file-to-file or file-to-socket).
  8. File Position Corruption: Concurrent modification of position by multiple threads without synchronization.
  9. Lock Effectiveness: Assuming file locks prevent access by processes not using the same locking mechanism.
  10. Buffer Underflow/Overflow: Not checking buffer limits before bulk operations.

Internal Implementation Details #

Thread Safety and Position Lock #

File channels are thread-safe for concurrent operations through careful synchronization:

// Simplified view of position-locked operations in FileChannelImpl
private int read(ByteBuffer dst) throws IOException {
    synchronized (positionLock) {
        ensureOpen();
        // ... perform I/O
        long p = this.position;
        int n = readImpl(dst, p);
        this.position = p + n;
        return n;
    }
}

private int read(ByteBuffer dst, long position) throws IOException {
    // Absolute read - doesn't need position lock for position update
    // but still synchronizes to prevent concurrent size changes
    synchronized (positionLock) {
        ensureOpen();
        return readImpl(dst, position);
    }
}

Native Thread Tracking #

The NativeThreadSet tracks threads blocked in native I/O operations to allow interruption and clean shutdown:

// When entering a blocking native I/O operation
int ti = threads.add();
try {
    // Perform native I/O (e.g., read, write, lock)
    return nd.read(fd, buf, position);
} finally {
    threads.remove(ti);
}

// When closing the channel, all tracked threads are interrupted
public void implCloseChannel() throws IOException {
    threads.signalAndWait();
    // ... close file descriptor
}

File Lock Table #

A system-wide FileLockTable manages locks to prevent overlapping locks within the same JVM:

// FileLockTable maintains mapping from FileKey to locks
interface FileLockTable {
    void add(FileLock fl) throws OverlappingFileLockException;
    void remove(FileLock fl);
    void replace(FileLock fl1, FileLock fl2);
    List<FileLock> removeAll();
}

// Each FileChannelImpl has a reference to the appropriate table
private FileLockTable fileLockTable() {
    if (fileLockTable == null) {
        synchronized (this) {
            if (fileLockTable == null) {
                fileLockTable = FileLockTable.newSharedTable();
            }
        }
    }
    return fileLockTable;
}

Transfer Optimization Tiers #

transferTo and transferFrom implement a three-tier optimization strategy:

// Tier 1: Direct transfer using OS-specific zero-copy
if ((n = transferToDirect(position, count, target)) >= 0) {
    return n;
}

// Tier 2: Mapped transfer for large regions to trusted channels
if (count >= MAPPED_TRANSFER_THRESHOLD && 
    count <= MAPPED_TRANSFER_SIZE &&
    target instanceof FileChannelImpl) {
    if ((n = transferToTrustedChannel(position, count, target)) >= 0) {
        return n;
    }
}

// Tier 3: Fallback to simple read-write loop
return transferToArbitraryChannel(position, count, target);

Memory Mapping Implementation #

Mapping a file region involves:

  1. Alignment rounding: Position rounded down to allocation granularity
  2. Size adjustment: Size increased to cover full pages
  3. File extension: For READ_WRITE mode, file extended if necessary
  4. Native mapping: nd.map() creates the memory mapping
  5. Cleaner registration: Unmapper registered to clean up mapping
// Simplified mapping logic
MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
    // Round position down to allocation granularity
    long mapPosition = position % allocationGranularity();
    long mapSize = size + (position - mapPosition);
    
    // Extend file if writing
    if (mode != MapMode.READ_ONLY) {
        long fileSize = size();
        if (mapPosition + mapSize > fileSize) {
            truncate(mapPosition + mapSize);
        }
    }
    
    // Create native mapping
    long address = nd.map(fd, mapModeToProt(mode), mapPosition, mapSize);
    
    // Create MappedByteBuffer with cleaner
    Unmapper unmapper = new Unmapper(address, mapSize, fd, mapPosition);
    return new DirectByteBuffer(mapSize, address, fd, unmapper, mode == MapMode.READ_ONLY);
}

Performance Optimization Techniques #

1. Batch Small Writes #

// Instead of many small writes:
for (byte[] chunk : chunks) {
    channel.write(ByteBuffer.wrap(chunk));
}

// Batch into single write:
ByteBuffer[] buffers = new ByteBuffer[chunks.length];
for (int i = 0; i < chunks.length; i++) {
    buffers[i] = ByteBuffer.wrap(chunks[i]);
}
channel.write(buffers);

2. Tune Mapping Thresholds #

// For large sequential reads, increase mapping threshold
public static final long CUSTOM_MAPPED_THRESHOLD = 32 * 1024L; // 32KB

if (fileSize > CUSTOM_MAPPED_THRESHOLD) {
    // Use memory mapping
    MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, fileSize);
    processMappedData(buffer);
} else {
    // Use regular read
    ByteBuffer buffer = ByteBuffer.allocate((int) fileSize);
    channel.read(buffer);
    buffer.flip();
    processBufferData(buffer);
}

3. Monitor with JFR Events #

FileChannel integrates with Java Flight Recorder for detailed I/O profiling:

// JFR events are automatically emitted for:
// - FileReadEvent: read operations
// - FileWriteEvent: write operations  
// - FileForceEvent: force operations
// - FileLockEvent: lock operations

// Enable with JVM flags:
// -XX:+FlightRecorder -XX:StartFlightRecording=filename=recording.jfr

4. Use Appropriate Access Patterns #

// Sequential access pattern:
ByteBuffer buffer = ByteBuffer.allocateDirect(64 * 1024); // 64KB buffer
while (channel.read(buffer) != -1) {
    buffer.flip();
    process(buffer);
    buffer.clear();
}

// Random access pattern:
MappedByteBuffer mapped = channel.map(MapMode.READ_ONLY, 0, channel.size());
for (long offset = 0; offset < channel.size(); offset += RECORD_SIZE) {
    processRecord(mapped, offset);
}

// Hybrid pattern for large files with hot regions:
Map<Long, MappedByteBuffer> hotRegions = new HashMap<>();
for (Long hotOffset : hotOffsets) {
    hotRegions.put(hotOffset, 
        channel.map(MapMode.READ_ONLY, hotOffset, REGION_SIZE));
}