Overview
Memory-mapped files allow you to map a region of a file directly into memory, enabling efficient file I/O operations. Java provides this capability through the FileChannel class and its map() method, which creates a MappedByteBuffer that represents the memory-mapped region.
Key Benefits
- High Performance: Bypasses traditional I/O system calls
- Direct Memory Access: File data accessed as if it were in memory
- OS Managed: Operating system handles paging and caching
- Shared Memory: Multiple processes can map the same file
Core Classes
FileChannel: Channel for reading, writing, mapping filesMappedByteBuffer: Buffer representing memory-mapped file regionFileInputStream/FileOutputStream: For creating file channels
Basic Usage
1. Creating Memory-Mapped Files
import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; public class MemoryMappedFileExample { public static void main(String[] args) throws Exception { // Method 1: Using RandomAccessFile createWithRandomAccessFile(); // Method 2: Using FileChannel.open() createWithFileChannelOpen(); } private static void createWithRandomAccessFile() throws Exception { try (RandomAccessFile file = new RandomAccessFile("example.dat", "rw"); FileChannel channel = file.getChannel()) { // Map the entire file (0 to file size) into memory MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_WRITE, // Read-write access 0, // Starting position channel.size() // Size to map ); // Work with the buffer workWithBuffer(buffer); } } private static void createWithFileChannelOpen() throws Exception { Path path = Paths.get("example2.dat"); try (FileChannel channel = FileChannel.open( path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { // Map file region MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_WRITE, 0, 1024 * 1024 // Map 1MB ); workWithBuffer(buffer); } } private static void workWithBuffer(MappedByteBuffer buffer) { // Write data buffer.putInt(12345); buffer.putDouble(3.14159); buffer.put("Hello Memory Mapping".getBytes()); // Read data - flip to switch from write to read mode buffer.flip(); int intValue = buffer.getInt(); double doubleValue = buffer.getDouble(); byte[] stringBytes = new byte[20]; buffer.get(stringBytes); String stringValue = new String(stringBytes).trim(); System.out.printf("Read values: %d, %.5f, %s%n", intValue, doubleValue, stringValue); } } Map Modes
public class MapModesExample { public static void demonstrateMapModes() throws Exception { Path filePath = Paths.get("testfile.dat"); // Create a file with some content try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) { // Write initial data ByteBuffer initData = ByteBuffer.allocate(100); initData.put("Initial file content".getBytes()); initData.flip(); channel.write(initData); } // READ_ONLY mode - for reading only try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ)) { MappedByteBuffer readOnlyBuffer = channel.map( FileChannel.MapMode.READ_ONLY, 0, channel.size()); // Can read but not write byte[] data = new byte[20]; readOnlyBuffer.get(data); System.out.println("Read: " + new String(data)); // readOnlyBuffer.put((byte) 1); // Would throw ReadOnlyBufferException } // READ_WRITE mode - for reading and writing try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.WRITE)) { MappedByteBuffer readWriteBuffer = channel.map( FileChannel.MapMode.READ_WRITE, 0, channel.size()); // Can both read and write readWriteBuffer.position(0); readWriteBuffer.put("Modified content".getBytes()); } // PRIVATE mode - copy-on-write (changes not propagated to file) try (FileChannel channel = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.WRITE)) { MappedByteBuffer privateBuffer = channel.map( FileChannel.MapMode.PRIVATE, 0, channel.size()); // Changes are private to this buffer privateBuffer.position(0); privateBuffer.put("Private modification".getBytes()); // File on disk remains unchanged } } } Practical Examples
Example 1: High-Performance File Copy
public class MemoryMappedFileCopy { public static void copyFile(String sourcePath, String destPath) throws Exception { Path source = Paths.get(sourcePath); Path destination = Paths.get(destPath); try (FileChannel sourceChannel = FileChannel.open(source, StandardOpenOption.READ); FileChannel destChannel = FileChannel.open(destination, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) { long size = sourceChannel.size(); // Map source file for reading MappedByteBuffer sourceBuffer = sourceChannel.map( FileChannel.MapMode.READ_ONLY, 0, size); // Map destination file for writing MappedByteBuffer destBuffer = destChannel.map( FileChannel.MapMode.READ_WRITE, 0, size); // Perform the copy destBuffer.put(sourceBuffer); // Force changes to disk destBuffer.force(); } } public static void main(String[] args) throws Exception { long startTime = System.currentTimeMillis(); copyFile("largefile.dat", "copy.dat"); long endTime = System.currentTimeMillis(); System.out.printf("File copied in %d ms%n", endTime - startTime); } } Example 2: Shared Memory Database-like Structure
public class MemoryMappedDatabase { private final MappedByteBuffer buffer; private final FileChannel channel; private static final int HEADER_SIZE = 1024; // bytes private static final int RECORD_SIZE = 128; // bytes private static final int MAX_RECORDS = 1000; public MemoryMappedDatabase(String filePath) throws Exception { Path path = Paths.get(filePath); this.channel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); long fileSize = HEADER_SIZE + (long) RECORD_SIZE * MAX_RECORDS; this.buffer = channel.map( FileChannel.MapMode.READ_WRITE, 0, fileSize ); initializeHeader(); } private void initializeHeader() { if (buffer.getInt(0) == 0) { // Check if uninitialized buffer.putInt(0, 1); // Version buffer.putInt(4, 0); // Record count } } public void addRecord(String data) { int recordCount = buffer.getInt(4); if (recordCount >= MAX_RECORDS) { throw new IllegalStateException("Database full"); } int recordOffset = HEADER_SIZE + recordCount * RECORD_SIZE; // Write record buffer.position(recordOffset); buffer.putLong(System.currentTimeMillis()); // timestamp byte[] dataBytes = data.getBytes(); buffer.putInt(dataBytes.length); // data length buffer.put(dataBytes); // actual data // Update record count buffer.putInt(4, recordCount + 1); // Force to disk buffer.force(); } public List<String> readAllRecords() { List<String> records = new ArrayList<>(); int recordCount = buffer.getInt(4); for (int i = 0; i < recordCount; i++) { int recordOffset = HEADER_SIZE + i * RECORD_SIZE; buffer.position(recordOffset); long timestamp = buffer.getLong(); int dataLength = buffer.getInt(); byte[] dataBytes = new byte[dataLength]; buffer.get(dataBytes); String record = String.format("[%tT] %s", Instant.ofEpochMilli(timestamp), new String(dataBytes)); records.add(record); } return records; } public void close() throws Exception { channel.close(); } public static void main(String[] args) throws Exception { MemoryMappedDatabase db = new MemoryMappedDatabase("database.dat"); // Add some records db.addRecord("First record"); db.addRecord("Second record"); db.addRecord("Third record"); // Read all records List<String> records = db.readAllRecords(); records.forEach(System.out::println); db.close(); } } Example 3: Concurrent Access with Read-Write Locks
public class ConcurrentMemoryMappedFile { private final MappedByteBuffer buffer; private final FileChannel channel; private final ReadWriteLock lock = new ReentrantReadWriteLock(); public ConcurrentMemoryMappedFile(String filePath, long size) throws Exception { Path path = Paths.get(filePath); this.channel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); this.buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, size); } public void writeData(int position, byte[] data) { lock.writeLock().lock(); try { buffer.position(position); buffer.put(data); buffer.force(); // Ensure data is written to disk } finally { lock.writeLock().unlock(); } } public byte[] readData(int position, int length) { lock.readLock().lock(); try { buffer.position(position); byte[] data = new byte[length]; buffer.get(data); return data; } finally { lock.readLock().unlock(); } } public void atomicIncrement(int position) { lock.writeLock().lock(); try { buffer.position(position); int value = buffer.getInt(); buffer.position(position); buffer.putInt(value + 1); buffer.force(); } finally { lock.writeLock().unlock(); } } public void close() throws Exception { channel.close(); } } // Usage example class ConcurrentExample { public static void main(String[] args) throws Exception { ConcurrentMemoryMappedFile sharedFile = new ConcurrentMemoryMappedFile("shared.dat", 1024); // Multiple threads can safely access the file ExecutorService executor = Executors.newFixedThreadPool(4); for (int i = 0; i < 10; i++) { final int threadId = i; executor.submit(() -> { sharedFile.atomicIncrement(0); // Safely increment counter sharedFile.writeData(4 + threadId * 10, ("Hello from thread " + threadId).getBytes()); }); } executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); sharedFile.close(); } } Advanced Techniques
1. Handling Large Files with Multiple Mappings
public class LargeFileHandler { private static final long MAX_MAP_SIZE = Integer.MAX_VALUE; // ~2GB public void processLargeFile(String filePath) throws Exception { Path path = Paths.get(filePath); try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { long fileSize = channel.size(); long position = 0; while (position < fileSize) { long size = Math.min(MAX_MAP_SIZE, fileSize - position); MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_ONLY, position, size); processBuffer(buffer, position); position += size; } } } private void processBuffer(MappedByteBuffer buffer, long fileOffset) { // Process the buffer chunk while (buffer.hasRemaining()) { // Read and process data byte b = buffer.get(); // Process byte... } } } 2. Memory-Mapped File with ByteBuffer Views
public class StructuredMemoryMap { private final MappedByteBuffer buffer; private final IntBuffer intView; private final LongBuffer longView; private final CharBuffer charView; public StructuredMemoryMap(String filePath, long size) throws Exception { Path path = Paths.get(filePath); try (FileChannel channel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) { this.buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, size); // Create different views of the same buffer this.intView = buffer.asIntBuffer(); this.longView = buffer.asLongBuffer(); this.charView = buffer.asCharBuffer(); } } public void writeStructuredData() { // Write integers intView.put(0, 42); intView.put(1, 100); // Write longs longView.put(2, 123456789L); // Note: position calculation considers element size // Write characters charView.put(20, 'H'); charView.put(21, 'i'); buffer.force(); } public void readStructuredData() { System.out.println("First int: " + intView.get(0)); System.out.println("Second int: " + intView.get(1)); System.out.println("Long: " + longView.get(2)); System.out.println("Chars: " + charView.get(20) + charView.get(21)); } } Performance Considerations
1. Benchmark Comparison
public class IOPerformanceBenchmark { public static void testTraditionalIO(String filePath, byte[] data) throws Exception { long start = System.nanoTime(); try (FileOutputStream fos = new FileOutputStream(filePath)) { for (int i = 0; i < 1000; i++) { fos.write(data); } } long duration = System.nanoTime() - start; System.out.printf("Traditional I/O: %d ns%n", duration); } public static void testMemoryMappedIO(String filePath, byte[] data) throws Exception { long start = System.nanoTime(); try (RandomAccessFile file = new RandomAccessFile(filePath, "rw"); FileChannel channel = file.getChannel()) { MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_WRITE, 0, data.length * 1000L); for (int i = 0; i < 1000; i++) { buffer.put(data); } buffer.force(); } long duration = System.nanoTime() - start; System.out.printf("Memory Mapped I/O: %d ns%n", duration); } public static void main(String[] args) throws Exception { byte[] testData = new byte[1024]; // 1KB Arrays.fill(testData, (byte) 1); testTraditionalIO("traditional.dat", testData); testMemoryMappedIO("mapped.dat", testData); } } Best Practices
- Always close channels in try-with-resources or finally blocks
- Use force() sparingly as it's expensive
- Handle large files by mapping portions at a time
- Consider memory usage when mapping large files
- Use appropriate map modes (READ_ONLY, READ_WRITE, PRIVATE)
- Be aware of platform limitations on maximum mapping size
Common Pitfalls
- Not calling force() when persistence is required
- Mapping files larger than available memory
- Forgetting to handle byte order in multi-platform environments
- Not considering concurrent access in multi-threaded applications
Memory-mapped files provide exceptional performance for file I/O operations and are particularly useful for large files, random access patterns, and shared memory scenarios.