skip to content

What are the main pitfalls of memory-mapped files in Java — resource cleanup, the 2 GB limit, and crash safety — and how do you address them?

level: seniorimportance: should knowfreq 34%

answer

  1. no portable unmap -> GC-bound cleanup, file stays locked (Windows delete/rename fails)
  2. ~2 GB per mapping -> window large files
  3. writes durable only after force()
  4. truncating under a live mapping -> SIGBUS crashes the JVM
  5. Java 21+ FFM API (MemorySegment/Arena) = deterministic close + >2 GB

basics

~20 s

Mapped files can't be reliably unmapped before the buffer is garbage-collected, so files may stay locked (notably on Windows). One mapping can't exceed about 2 GB. And writes aren't safe on disk until you call force(). Plan for all three.

solid answer

~50 s

Three pitfalls dominate memory-mapped I/O in Java. First, cleanup is non-deterministic: there's no portable public unmap(), so a MappedByteBuffer's OS resources are freed only when the buffer is garbage-collected. Until then the file stays mapped — on Windows you often can't delete or rename it, and you can't shrink/truncate it safely. Second, a single mapping is capped at Integer.MAX_VALUE (~2.1 GB) because ByteBuffer capacity is an int, so large files need multiple windowed mappings. Third, mapped writes go to the page cache and are durable only after force(); a power loss before flush loses data, and accessing a region after the file was truncated underneath you can crash the JVM with a SIGBUS. Mitigations: keep mapped buffers short-lived and let them go out of scope (or use the Java 21+ Foreign Function & Memory API / MemorySegment for deterministic close and >2 GB mappings), map in fixed-size windows, and force() at checkpoints with careful truncation handling.

go deeper

for a junior

Knows mapped writes need force() to be saved and that one mapping can't cover huge files.

for a middle

Explains the GC-bound unmap problem and Windows file-lock symptom, the int-capacity 2 GB cap, and windowing large files.

for a senior

Discusses SIGBUS-on-truncation crashing the JVM, durability via force() at checkpoints, and migrating to the FFM API for deterministic cleanup and large mappings.

for a principal

Architects mmap-based storage (windowing strategy, crash-consistent flush/truncation protocols, resource accounting), weighs mmap vs. plain channel I/O at scale, and standardizes on FFM/MemorySegment with confined arenas.

## Why memory mapping bites back Memory mapping is powerful but leaks abstraction: you're manipulating OS virtual-memory state through a Java object whose lifecycle is governed by the garbage collector, not by you. Three concrete pitfalls follow. ### 1. Non-deterministic cleanup (the unmap problem) Standard Java provides **no public, portable `unmap()`** for a `MappedByteBuffer` (before the FFM API). The underlying OS mapping is torn down only when the buffer becomes **unreachable and is garbage-collected**, and even then at the GC's discretion. Consequences: - The file remains *mapped* until GC runs. On **Windows**, a mapped file typically cannot be **deleted, renamed, or truncated** — so a `Files.delete(path)` may fail with the file "in use" even after you closed the channel. - You cannot deterministically free the address-space and page-cache resources, which matters for many/large mappings. Workarounds historically used the internal `sun.misc.Cleaner`/`Unsafe.invokeCleaner` hack — fragile and discouraged. The clean modern answer is the **Foreign Function & Memory API** (`java.lang.foreign`, stable in Java 21+): `Arena.ofConfined()` plus `FileChannel.map(mode, off, size, arena)` gives a `MemorySegment` whose mapping is **deterministically released when the Arena is closed** (try-with-resources). If you must stay on classic NIO, the practical advice is: keep `MappedByteBuffer`s **short-lived and out of scope**, minimize their count, and don't rely on freeing them at a precise moment. ### 2. The ~2 GB per-mapping limit `FileChannel.map` takes a `long size`, but a `MappedByteBuffer` extends `ByteBuffer`, whose **capacity is an `int`**. Maximum single mapping = `Integer.MAX_VALUE` ≈ 2.1 GB. To map a larger file with classic NIO you **window** it: create several mappings of ≤2 GB each and route each access to the mapping covering that offset. This adds bookkeeping (which window, boundary-straddling reads). The FFM API removes this limit — `MemorySegment` mappings use `long` sizing. ### 3. Crash safety and truncation hazards - **Durability:** mapped writes land in the page cache and are written back lazily; they are durable only after `force()` (see the force/durability topic). A power loss before flush loses them. - **SIGBUS on shrink:** if the underlying file is **truncated** (by you or another process) so that a mapped region no longer corresponds to real file contents, touching that region can deliver a **SIGBUS**, which typically **crashes the JVM** (it's a hardware-level signal, not a catchable Java exception). So never shrink a file out from under a live mapping; coordinate truncation with mapping lifecycle, and on some platforms you must also drop the mapping before changing file length. - **Stale views:** other processes mapping/writing the same file can change data you're reading; combine with `FileLock` if you need consistency. ## Putting it together: a safe pattern ```java // Classic NIO: window large files, force at checkpoints, scope the buffer tightly. long chunk = Integer.MAX_VALUE; // ~2 GB for (long pos = 0; pos < size; pos += chunk) { long len = Math.min(chunk, size - pos); MappedByteBuffer w = ch.map(READ_WRITE, pos, len); // ... process window ... w.force(); // durability at the window boundary // let w go out of scope; do not truncate the file while mappings live } ``` For new code on Java 21+, prefer the FFM API for deterministic unmap and large mappings. ## Summary The three classic memory-mapping pitfalls are: (1) cleanup is GC-bound with no portable unmap (file stays locked, esp. on Windows); (2) one mapping is capped at ~2 GB; (3) writes aren't durable until force(), and truncating a file under a live mapping can SIGBUS-crash the JVM. Mitigate with short-lived/windowed mappings, force() at checkpoints, careful truncation handling, or the Java 21+ FFM API for deterministic close and large mappings.

  • Why does deleting a memory-mapped file sometimes fail on Windows even after closing the channel?
    Because the OS mapping is released only when the MappedByteBuffer is garbage-collected, not when the channel closes. Until GC runs, Windows considers the file in use and blocks delete/rename. Let the buffer go out of scope (or use the FFM API's Arena for deterministic release).
  • What happens if another process truncates the file while you hold a live mapping and you access the now-missing region?
    You can get a SIGBUS, a hardware-level signal that typically crashes the whole JVM and isn't catchable as a Java exception. You must coordinate truncation with mapping lifecycle and avoid shrinking a file under a live mapping.

saying these in an interview costs you the question

  • Assuming closing the FileChannel immediately unmaps and frees the file
  • Relying on a single mapping for files over ~2 GB
  • Truncating/shrinking a file while a mapping over that region is still live (SIGBUS risk)
  • Believing mapped writes are durable without force()
  • Recommending sun.misc.Unsafe.invokeCleaner as the normal, supported way to unmap

context