Skip to Content
Course content

99: Working with Memory-Mapped Files

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're working in a massive archive with thousands of heavy ledger books. If you want to change one number on page 400 of Volume 12, the traditional way (standard I/O) is like calling a librarian, asking them to go fetch the book, carry it to your desk, let you make the change, and then carry it all the way back to the shelf. Every single time you need a different page, that's another trip for the librarian.

Memory-mapped files change the game. Instead of the librarian moving books, it's as if you've laid the entire ledger out on a magically expanding table that covers your whole office. You don't "read" the book into your hands; you just slide your eyes to the exact spot on the table where page 400 is and write your change. The "magic" happens in the background—the OS handles bringing the actual pages from the disk into RAM only when you look at them, and it flushes your changes back to the disk without you having to call write() manually.

Turning Disk Space into Memory Addresses

In Java, we achieve this using the FileChannel class and the MappedByteBuffer. You aren't loading the file into the Java Heap (which is where your normal objects live); instead, you're mapping the file into virtual memory. This is why you can "map" a 10GB file even if your JVM only has 2GB of heap allocated.

Here is how I usually set this up when I need to handle a large binary data file—for example, a custom database file where I know exactly where each record starts.

import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class LogModifier {
    public static void main(String[] args) throws Exception {
        // We use RandomAccessFile to get a channel that supports both reading and writing
        try (RandomAccessFile file = new RandomAccessFile("data.bin", "rw")) {
            FileChannel channel = file.getChannel();

            // Map the first 1KB of the file into memory
            // MapMode.READ_WRITE means changes to the buffer are persisted to disk
            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 1024);

            // I can treat this file like a giant array. 
            // Let's say we want to change the byte at position 500 to 'A'
            buffer.put(500, (byte) 'A');
            
            // We can also read directly from a specific offset
            byte value = buffer.get(500);
            System.out.println("The value at 500 is: " + (char)value);
        }
    }
}

The OS is Doing the Heavy Lifting

You might be wondering: "Wait, if I'm just putting a byte into a buffer, when does it actually hit the disk?"

That's the beauty of it. The Operating System's virtual memory manager handles the "paging." When you call buffer.put(), you're changing a page in RAM. The OS marks that page as "dirty." At some point, the OS decides it's time to write dirty pages back to the disk. If you're in a situation where you absolutely cannot afford to lose data (like a transaction log), you can call buffer.force() to tell the OS: "Stop waiting, write this to the platter right now."

The Unmapping Headache

Now, here is a bit of a "gotcha" that has frustrated me and many other Java devs for years: Java does not have a clean, public API to unmap a file immediately. The mapping stays active until the MappedByteBuffer object is garbage collected.

This is a huge pain on Windows, where the OS will prevent you from deleting or renaming a file if it's still mapped. You'll find yourself staring at a FileSystemException even though you've closed your FileChannel. In professional production code, you'll often see people using "unsafe" hacks (via sun.misc.Unsafe) to manually trigger the cleaner, but for most applications, just knowing that the mapping is tied to the object's lifecycle is enough to keep you out of trouble.

When should you actually use this?

Don't use this for every file. For small config files or simple text logs, Files.readAllLines() or a BufferedReader is perfectly fine. I reach for memory-mapped files when:

  • The file is massive (gigabytes), and I only need to access small, random chunks of it.
  • I need extremely low latency and want to avoid the overhead of repeated system calls (read/write).
  • I'm implementing a shared-memory mechanism between two different JVMs on the same machine.



📋 Practical Task

Exercise: Building a High-Speed Binary Record Editor

Your task is to create a program that manages a binary file acting as a simple database of "User Scores." Each record consists of a 4-byte integer (the User ID) followed by a 4-byte integer (the Score), making each record exactly 8 bytes long.

Requirements:

  • Create a file named scores.dat and pre-populate it with at least 10 records (80 bytes total) using a FileOutputStream or FileChannel.
  • Implement a method updateScore(int recordIndex, int newScore) that uses a MappedByteBuffer to locate the specific record and update only the score portion (the last 4 bytes of that record) without rewriting the entire file.
  • Implement a method readScore(int recordIndex) that uses the mapping to retrieve the score for a specific user.
  • Ensure you use FileChannel.MapMode.READ_WRITE.
  • Verify your changes by reading the file back using a standard FileInputStream to prove the data was persisted to disk.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.