Java gives you no free() for a direct byte buffer. How is that native memory actually released, what bounds how much of it a JVM may hold, and what happens when that bound is reached?
answer
- No free(); cleanup action tied to wrapper reachability
- Release is post-mortem, on the collector's schedule
- MaxDirectMemorySize defaults to about -Xmx
- Reservation path: help cleanups, request GC, retry, then OOM
- DisableExplicitGC breaks the fallback; pooling removes the dependency
basics
~20 sEach direct buffer registers a cleanup action that frees its native block only after the buffer object becomes unreachable and the collector processes it. The total is bounded by -XX:MaxDirectMemorySize, defaulting to about the maximum heap; exceeding it throws OutOfMemoryError: Direct buffer memory.
solid answer
~50 sRelease is **post-mortem and indirect**. The small Java wrapper object is registered with cleanup machinery holding the native address; once the wrapper is unreachable and the collector has processed it, that action calls the native free. There is no supported `free()` in the public API, so the native block's lifetime is exactly the wrapper's reachability plus an unbounded delay. The budget is `-XX:MaxDirectMemorySize`, defaulting to roughly `-Xmx`. When a reservation would exceed it, the JVM does not fail immediately: it attempts to help pending cleanups along and, as a fallback, requests a collection and retries with backoff, because the memory it needs may be held by unreachable-but-not-yet-processed buffers. If retries fail it throws `OutOfMemoryError: Direct buffer memory`. Two operational consequences: `-XX:+DisableExplicitGC` neuters that fallback and turns recoverable pressure into failures, and a heap under no pressure may not collect often enough to release direct memory. Pooling buffers removes the dependency on collection timing.
code
text · 6 lines-XX:MaxDirectMemorySize=512m # explicit bound (default: about the max heap)
-XX:+ExplicitGCInvokesConcurrent # keep the reclaim trigger without a full STW cycle
# avoid: -XX:+DisableExplicitGC # removes the fallback that reclaims direct memory
jcmd <pid> VM.native_memory summary # requires -XX:NativeMemoryTracking=summary at startup
# java.nio:type=BufferPool,name=direct -> Count / MemoryUsed / TotalCapacitygo deeper
Know there is no free: the native block is released after the buffer object becomes unreachable and cleanup runs, and the budget is a separate flag from -Xmx.
Describe the reservation path, the default budget of roughly the max heap, and the resulting OutOfMemoryError: Direct buffer memory.
Separate retention from timing from sizing when diagnosing, explain why disabling explicit collections breaks recovery, and argue pooling as the durable fix.
Set policy: explicit native budgets per service, ownership of native lifetimes by pooling or explicit-lifetime APIs, and monitoring that treats native usage as a first-class capacity dimension.
## Why there is no free() A direct buffer is two things: a native block of bytes, and a small Java object holding its address and index state. Exposing an explicit `free()` on the Java object would let one thread release memory another thread is still reading through a live buffer, turning a Java-level bug into a process crash or silent corruption. The platform's answer was to tie the native block's lifetime to the **reachability of the wrapper**, which the collector already tracks safely. Mechanically, the buffer registers a cleanup action associated with its wrapper object. When the collector determines the wrapper is unreachable, that action is queued and run by a cleanup thread, which calls the native free with the recorded address. The result is correct but has a property worth stating plainly: **native memory is released on the collector's schedule, not on your code's.** A buffer dropped at 10:00 may hold its native block until whenever a collection cycle notices, which on a large, idle heap can be a long time. ## The budget and the reservation path Before allocating, the JVM reserves against a global counter bounded by `-XX:MaxDirectMemorySize`. Its default is essentially the maximum heap size, which surprises people: a JVM with `-Xmx4g` may hold roughly four more gigabytes of direct buffers, so the process can approach twice the heap figure before anything complains. When a reservation would exceed the bound, the JVM does not fail on the spot. Because the memory it needs may belong to buffers that are already garbage but not yet cleaned, the reservation path first tries to make progress on pending cleanup actions. If that is insufficient, it falls back to requesting a full collection, then retries the reservation in a loop with increasing sleeps. Only when that loop is exhausted does it throw `OutOfMemoryError: Direct buffer memory`. This is one of the few places in the platform where correctness of a *memory budget* depends on a collection actually happening, and it explains two field behaviours. - **`-XX:+DisableExplicitGC` is hazardous here.** Applications often set it to stop libraries calling for collections. It also disables the fallback in this path, so a workload that previously recovered from a burst of direct-buffer churn now throws instead. If explicit collections must be suppressed, `-XX:+ExplicitGCInvokesConcurrent` keeps the trigger while avoiding a stop-the-world full collection. - **A healthy heap can starve direct memory.** If the heap is comfortably sized and collections are rare, unreachable buffers accumulate their native blocks unreleased, and the process footprint grows even though nothing is leaking in the Java sense. ## The failure looks like a leak but often is not Three distinct situations produce the same symptom of growing native memory: 1. **A real retention bug.** Buffers are referenced by a cache, a pool that never releases, or a static registry. The wrapper stays reachable, so the native block is never freed. This is a leak in the ordinary sense. 2. **A timing problem.** Buffers are unreachable but collections are too infrequent, so cleanup lags allocation. Adding pressure, or simply allocating more slowly, resolves it. 3. **A sizing problem.** Legitimate concurrent demand exceeds the budget: too many connections times too large a per-connection buffer. They are distinguished by what happens after a collection: if a forced cycle drops native usage sharply, it is timing or sizing; if it does not move, something still references the buffers. ## The reliable pattern: pooling and explicit lifetimes The robust answer is to stop depending on the collector for native lifetimes. - **Pool and reuse buffers.** Allocate a bounded set once, check them out and back in. Native footprint becomes a constant chosen by you, allocation cost is amortised, and no cleanup timing matters. This is exactly why networking libraries provide pooled buffer allocators and reference-counted buffers. - **Bound and monitor the budget explicitly.** Set `-XX:MaxDirectMemorySize` deliberately rather than inheriting the heap-shaped default, and watch the platform's direct-buffer pool metrics, which expose count, used bytes and capacity. Native memory tracking (`-XX:NativeMemoryTracking=summary` plus `jcmd <pid> VM.native_memory`) attributes native usage by category when the pool metrics are not enough. - **Use explicit-lifetime APIs for new code.** The foreign function and memory API gives deterministic release through arenas with safety checks, which removes this entire class of problem for memory you allocate yourself. ## Answer shape Say: no free, cleanup tied to wrapper reachability, so release is post-mortem and collector-scheduled; budget is `MaxDirectMemorySize` defaulting to about the max heap; the reservation path helps cleanups and falls back to requesting a collection with retries before throwing `OutOfMemoryError: Direct buffer memory`; therefore disabling explicit collections is dangerous and pooling is the durable fix.
- A service throws OutOfMemoryError: Direct buffer memory while the heap is barely used. What are the candidate causes and how do you tell them apart?Either buffers are still referenced (a genuine retention bug), or they are unreachable but collections are too rare for cleanup to keep pace, or concurrent demand legitimately exceeds the budget. Force or observe a collection and watch the direct-buffer pool metrics: if used bytes fall sharply, it is a timing or sizing issue and pooling or a larger bound is the fix; if they do not move, find what still references the buffers.
- Why is -XX:+DisableExplicitGC risky for an application that uses direct buffers heavily?When the direct-memory reservation cannot be satisfied, the JVM's fallback is to request a collection so that unreachable buffers get cleaned and their native blocks released, then retry. Disabling explicit collections removes that step, so pressure that used to recover becomes an OutOfMemoryError. If suppressing library-triggered full collections is the goal, ExplicitGCInvokesConcurrent keeps the trigger while making it concurrent.
saying these in an interview costs you the question
- Saying direct memory is freed 'when the buffer goes out of scope', as if it were deterministic
- Believing -Xmx or the heap limit bounds direct memory
- Recommending -XX:+DisableExplicitGC as a general tuning improvement without considering this path
- Claiming the only fix for direct-memory exhaustion is a bigger limit
- Assuming a heap dump shows the native bytes held by direct buffers