What is the difference between a direct ByteBuffer and a heap (non-direct) ByteBuffer, and when would you choose each?
answer
- heap = byte[] on GC heap; direct = native off-heap memory
- direct avoids the extra heap<->native copy in channel I/O
- direct allocateDirect is expensive -> reuse/pool
- direct freed via Cleaner/PhantomRef, not prompt; own MaxDirectMemorySize
- array() works on heap only; both share the same marker API
basics
~20 sA heap buffer stores its bytes in a normal Java array on the garbage-collected heap. A direct buffer stores them in native (off-heap) memory the OS can access directly. Direct buffers make channel I/O faster because the JVM can skip an extra copy, but they cost more to allocate and free.
solid answer
~50 sByteBuffer.allocate(n) gives a heap buffer backed by a byte[] on the Java heap; it is cheap to allocate, GC-managed, and exposes array()/arrayOffset(). ByteBuffer.allocateDirect(n) gives a direct buffer whose storage lives in native memory outside the GC heap. The payoff: when you do channel I/O, the OS reads/writes native memory directly, so a direct buffer avoids the hidden copy the JVM otherwise makes between the heap array and a temporary native staging buffer (the JVM keeps a per-thread pool of these for heap buffers). Direct buffers cost more to create and rely on Cleaner/PhantomReference (or, historically, finalization) to free off-heap memory, so they are not GC-prompt and can build up off-heap pressure. Rule of thumb: use direct buffers for large, long-lived buffers in hot I/O paths (sockets, file channels); use heap buffers for small, short-lived, or compute-heavy buffers. Both share the identical position/limit/capacity/mark API.
go deeper
Knows direct buffers live outside the normal Java heap and are used for faster I/O.
Explains the extra copy avoided by direct buffers and that direct allocation is more expensive.
Weighs allocation cost vs I/O savings, knows direct memory is freed via Cleaner (not prompt) with its own size limit, and recommends pooling/reuse.
Reasons about off-heap memory management strategy, GC interaction (object pinning, Cleaner phantom refs), MaxDirectMemorySize tuning, and when to reach for MappedByteBuffer or a custom pool at scale.
## Two places the bytes can live A `ByteBuffer` is just an API over some block of memory plus the four cursors (capacity/limit/position/mark). The factory method decides *where* that block lives: - **`ByteBuffer.allocate(n)`** → a **heap buffer**. Its bytes sit in an ordinary Java `byte[]` array on the **garbage-collected heap** — the same memory region as all your normal objects. You can even reach the array via `array()` / `arrayOffset()` (only when `hasArray()` is true). - **`ByteBuffer.allocateDirect(n)`** → a **direct buffer**. Its bytes sit in **native (off-heap) memory**, allocated outside the Java heap (think `malloc`-style memory the OS owns). `array()` throws `UnsupportedOperationException` because there is no Java array behind it. ## Why direct buffers can be faster for I/O The operating system's read/write system calls operate on native memory addresses. A garbage collector, however, is allowed to **move** Java objects (including `byte[]` arrays) around to compact the heap. You cannot hand a moving array's address to the OS for the duration of a blocking I/O call. So when you do `channel.read(heapBuffer)`, the JVM cannot let the OS write straight into the heap array. Instead it: 1. allocates (or reuses from a per-thread pool) a **temporary direct buffer**, 2. has the OS read into that native buffer, 3. **copies** the bytes from the native buffer into your heap array. That extra copy is the cost. With a **direct** buffer, its memory is already native and pinned (won't be moved by GC), so the OS reads straight into it — **no extra copy**. For large, frequent transfers this is a real win. ## The costs of direct buffers - **Allocation is expensive.** `allocateDirect` calls into native allocation and zeroes the memory; it is much slower than `allocate`, so you should *reuse* direct buffers, not churn them. - **Freeing is not prompt.** Off-heap memory is reclaimed only when the Java `DirectByteBuffer` wrapper object is collected, which triggers a `Cleaner` (a `PhantomReference`-based hook; historically `finalize`). Because the wrapper is tiny, GC feels little pressure to collect it, so the large native block can linger. This causes the classic "my heap is small but the process RSS keeps growing / `OutOfMemoryError: Direct buffer memory`" problem. - **Bounded by `-XX:MaxDirectMemorySize`.** Direct memory has its own ceiling, separate from `-Xmx`. - **No backing array**, so byte-twiddling code that wants `array()` must use a heap buffer. ## Choosing | factor | heap (allocate) | direct (allocateDirect) | |---|---|---| | storage | byte[] on GC heap | native off-heap memory | | allocation cost | cheap | expensive | | I/O copy | extra heap<->native copy | none (zero intermediate copy) | | GC | normal, prompt | wrapper is GC'd, native freed via Cleaner (not prompt) | | array() access | yes (hasArray()) | no | | best for | small/short-lived/compute buffers | large/long-lived buffers in hot I/O paths | **Rule of thumb:** if a buffer is large and used repeatedly for socket or file channel I/O, allocate it direct **once and reuse it** (or use a pool). For small, transient, or compute-only buffers, heap is simpler and cheaper. `MappedByteBuffer` (from `FileChannel.map`) is a special direct buffer backed by a memory-mapped file. Importantly, **both kinds expose the exact same Buffer API** — capacity/limit/position/mark, flip/clear/compact, get/put. "Direct" only changes *where the bytes live and how I/O touches them*, never how you index them.
- Why can't the OS write directly into a heap ByteBuffer's array?The garbage collector may relocate the array to compact the heap, so its address is not stable for the duration of a blocking syscall. The JVM therefore stages I/O through a temporary native buffer and copies into the heap array.
- What causes 'OutOfMemoryError: Direct buffer memory'?Off-heap memory backing direct buffers is reclaimed only when the small wrapper objects are GC'd (via Cleaner). If you allocate many direct buffers faster than they are collected, or exceed -XX:MaxDirectMemorySize, native allocation fails even though the Java heap is fine.
saying these in an interview costs you the question
- Claiming direct buffers are always faster (allocation is costly; only I/O-bound reuse pays off)
- Thinking direct buffers are freed immediately when you stop using them
- Calling array() on a direct buffer (throws UnsupportedOperationException)
- Believing direct memory counts against -Xmx (it has its own MaxDirectMemorySize ceiling)
- Allocating a fresh direct buffer per request instead of pooling/reusing