Skip to content

Latest commit

 

History

History
172 lines (146 loc) · 8.98 KB

memory-management.adoc

File metadata and controls

172 lines (146 loc) · 8.98 KB

Chronicle Bytes Memory Management

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.

1. Reference Counting

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 throws IllegalStateException.

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.

Reference counting lifecycle (conceptual)
stateDiagram-v2
    [*] --> Unallocated
    Unallocated --> Allocated: reserve()
    Allocated --> Allocated: reserve()
    Allocated --> Allocated: release()
    Allocated --> Freed: releaseLast() / refCount == 0
    Freed --> [*]

2. Native Memory

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.

3. Memory-Mapped Files

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.

4. On-Heap Stores

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.

5. Resource Tracing

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.

6. Elastic Buffers

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.

7. Best Practices

  • Always release native or mapped buffers in a finally block 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 Bytes instance.

  • 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: