Chronicle Bytes allocates and manages memory outside the Java heap to minimise GC pauses. This guide explains how that memory is handled and how to avoid leaks or stalls when working with off-heap buffers.
All off-heap stores implement ReferenceCounted.
Each call to reserve()
increments the count.
release() decrements it and releaseLast() frees the
resource when the count reaches zero [CB-NF-O-001].
Common pitfalls:
-
Forgetting to call
release()leads to native memory leaks. -
Calling
release()too many times throwsIllegalStateException.
Use try-with-resources around Closeable Bytes objects or manage ownership via
ReferenceOwner to avoid mistakes.
ReferenceCountedTracer can help debug
leaks by recording where a store was reserved.
stateDiagram-v2
[*] --> Unallocated
Unallocated --> Allocated: reserve()
Allocated --> Allocated: reserve()
Allocated --> Allocated: release()
Allocated --> Freed: releaseLast() / refCount == 0
Freed --> [*]
NativeBytesStore allocates direct memory using Unsafe.
releaseLast()
returns the memory to the OS. For mapped files MappedBytesStore delegates to
MappedFile which may unmap via an internal Unmapper or the JDK
Cleaner.
Failure to release prevents the memory from being reclaimed.
MappedFile maps a file region into memory.
Acquire a MappedBytes view to
read or write.
When finished, call releaseLast() on both the MappedBytes and
its store.
Chunked mappings use fixed-size regions, while single mappings cover
an entire file.
Synchronisation with the filesystem is controlled via
SyncMode.
Primitive reads and atomic swaps need their entire value inside one chunk mapping,
including any overlap (CB-NF-S-001). If acquiring the chunk for the offset still
leaves part of the value outside that mapping, ChunkedMappedBytes throws
DecoratedBufferUnderflowException, matching its existing write-side range check.
This includes volatile reads and peeks, and long compare-and-swap, where Core already refuses a
straddling swap as misaligned and the check only unifies the exception type. A rejected primitive read
preserves the read position, but may switch the current mapping. A copy that fails while acquiring a
later mapping can leave partial output; a streaming copy can also advance the read position.
unsafeRead and unsafeReadObject reject insufficient logical input before consuming or copying bytes.
Byte-wise reads can continue into the next chunk, and byte-array copies
(read(byte[]), read(offset, byte[], arrayOffset, length), copyTo(byte[])) are performed in pieces
that each stay inside one mapping. read(Bytes, length) and readWithLength(Bytes) still copy full
eight-byte words, so they reject a word that straddles a zero-overlap chunk end.
hash(length) requires the entire range to be inside the current mapping, including any overlap.
Otherwise it throws UnsupportedOperationException without copying the range or acquiring another chunk.
Copying a potentially multi-terabyte mapped range into a retained direct buffer would be unreasonable.
A random read can select another chunk without moving the cursor; call readPosition(readPosition())
to reselect the cursor’s chunk before hashing a range that fits there. Rejection preserves the read position,
read limit and current mapping. Hash values within one mapping and heap hashes are unchanged.
The native hash guard also applies to the public OptimisedBytesStoreHash.applyAsLongAny and
applyAsLong32bytesMultiple entry points; their private implementations share the validated dispatch path.
Views returned by MappedBytes.bytesForRead() and bytesForWrite() retain an intermediate MappedBytes.
Contiguity checks follow that backing chain to the physical store, including its lower bound, without remapping.
The intermediate view’s logical start and whole-file capacity cannot prove a native range safe.
Copies and comparisons through either view use the same bounded fallbacks as their mapped reader.
If a source view shares the destination’s mapped reader, selecting the destination can replace the source mapping.
The direct-copy path rechecks the source and obtains its address after that selection; otherwise it uses checked reads.
Copies that read from a MappedBytes source (write(offset, RandomDataInput, …), write(BytesStore, offset,
length), the streaming write(bytes, offset, length), unsafeRead and unsafeReadObject) go through checked
reads when the source range is not inside its current chunk. equalBytes compares the part of a range beyond the
current chunk byte by byte. Ordinary equals and startsWith also use checked byte reads when their
compared ranges do not fit the current mappings, including when vectorisation is disabled.
Equality retains its zero-filled unallocated-tail semantics, and comparisons preserve the read positions.
peekUnsignedByte(offset) answers for every chunk, not only the first. A direct store returns -1 for
an offset outside its mapping before translating the address, including with assertions enabled. A copy from any
Bytes whose range exceeds its store throws BufferUnderflowException instead of reading past the store. On every
backend, stop-character UTF-8 parsing ends on a clean end of input between complete characters, rejects the
overlong lead bytes 0xC0 and 0xC1 and three-byte overlong encodings starting with E0 80..9F,
and throws on truncated characters or invalid continuation bytes. The stop-character decoder retains
three-byte surrogate code units (ED A0..BF 80..BF), as emitted by appendUtf8Char; it is not a strict
Unicode-scalar UTF-8 validator.
HeapBytesStore wraps a byte[].
The JVM manages the array but the surrounding
Bytes object may still need explicit release if it holds other off-heap
resources.
Enable Jvm.isResourceTracing() during testing to record stack traces for
allocations.
This helps locate leaks if releaseLast() is not called.
AbstractReferenceCounted implements ReferenceCountedTracer, so classes such
as NativeBytes can call createdHere() to recover the stack trace of the
allocation site.
Chronicle Bytes feeds that trace into diagnostics like
DecoratedBufferOverflowException in NativeBytes.newDBOE(…), which means a
resizing failure reports both the write that triggered it and where the buffer
originated.
This is invaluable when multiple components reuse elastic buffers
and you need to pinpoint which call chain produced a leaked or overgrown
instance.
The richer exception context fulfils the diagnostic requirement in
[CB-NF-O-006].
When tracing is disabled createdHere() returns null, so
Chronicle Bytes automatically falls back to the standard constructor to avoid
raising a misleading NullPointerException.
Elastic Bytes can grow when more space is required.
For native buffers a new
NativeBytesStore is allocated and the existing data copied.
On-heap elastic
buffers resize the underlying array similarly.
Pre-sizing reduces the overhead of
the first expansion.
-
Always release native or mapped buffers in a
finallyblock or via try-with-resources. -
Keep references to stores short-lived when possible to lower the risk of leaks.
-
Use atomic operations on the store for thread-safe updates rather than sharing a single mutable
Bytesinstance. -
Monitor application logs for warnings about unreleased stores when resource tracing is enabled.
For a wider architectural context on how these practices are applied in higher-level libraries, see: