blob: 58ef382dc9dabace5d6eb5f6d6a587c48651fd00 [file] [log] [blame]
.. Licensed to the Apache Software Foundation (ASF) under one
.. or more contributor license agreements. See the NOTICE file
.. distributed with this work for additional information
.. regarding copyright ownership. The ASF licenses this file
.. to you under the Apache License, Version 2.0 (the
.. "License"); you may not use this file except in compliance
.. with the License. You may obtain a copy of the License at
.. http://www.apache.org/licenses/LICENSE-2.0
.. Unless required by applicable law or agreed to in writing,
.. software distributed under the License is distributed on an
.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
.. KIND, either express or implied. See the License for the
.. specific language governing permissions and limitations
.. under the License.
=================
Memory Management
=================
The memory modules contain all the functionality that Arrow uses to allocate and deallocate memory. This document is divided in two parts:
The first part, *Memory Basics*, provides a high-level introduction. The following section, *Arrow Memory In-Depth*, fills in the details.
Memory Basics
=============
This section will introduce you to the major concepts in Java’s memory management:
* `ArrowBuf`_
* `BufferAllocator`_
* Reference counting
It also provides some guidelines for working with memory in Arrow, and describes how to debug memory issues when they arise.
Getting Started
---------------
Arrow's memory management is built around the needs of the columnar format and using off-heap memory.
Arrow Java has its own independent implementation. It does not wrap the C++ implementation, although the framework is flexible enough
to be used with memory allocated in C++ that is used by Java code.
Arrow provides multiple modules: the core interfaces, and implementations of the interfaces.
Users need the core interfaces, and exactly one of the implementations.
* ``memory-core``: Provides the interfaces used by the Arrow libraries and applications.
* ``memory-netty``: An implementation of the memory interfaces based on the `Netty`_ library.
* ``memory-unsafe``: An implementation of the memory interfaces based on the `sun.misc.Unsafe`_ library.
ArrowBuf
--------
ArrowBuf represents a single, contiguous region of `direct memory`_. It consists of an address and a length,
and provides low-level interfaces for working with the contents, similar to ByteBuffer.
Unlike (Direct)ByteBuffer, it has reference counting built in, as discussed later.
Why Arrow Uses Direct Memory
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* The JVM can optimize I/O operations when using direct memory/direct buffers; it will attempt to avoid copying buffer contents to/from an intermediate buffer. This can speed up IPC in Arrow.
* Since Arrow always uses direct memory, JNI modules can directly wrap native memory addresses instead of copying data. We use this in modules like the C Data Interface.
* Conversely, on the C++ side of the JNI boundary, we can directly access the memory in ArrowBuf without copying data.
BufferAllocator
---------------
The `BufferAllocator`_ is primarily an arena or nursery used for accounting of buffers (ArrowBuf instances).
As the name suggests, it can allocate new buffers associated with itself, but it can also
handle the accounting for buffers allocated elsewhere. For example, it handles the Java-side accounting for
memory allocated in C++ and shared with Java using the C-Data Interface. In the code below it performs an allocation:
.. code-block:: Java
import org.apache.arrow.memory.ArrowBuf;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
try(BufferAllocator bufferAllocator = new RootAllocator(8 * 1024)){
ArrowBuf arrowBuf = bufferAllocator.buffer(4 * 1024);
System.out.println(arrowBuf);
arrowBuf.close();
}
.. code-block:: shell
ArrowBuf[2], address:140363641651200, length:4096
The concrete implementation of the BufferAllocator interface is `RootAllocator`_. Applications should generally create
one RootAllocator at the start of the program, and use it through the BufferAllocator interface. Allocators implement
AutoCloseable and must be closed after the application is done with them; this will check that all outstanding memory
has been freed (see the next section).
Arrow provides a tree-based model for memory allocation. The RootAllocator is created first, then more allocators
are created as children of an existing allocator via `newChildAllocator`_. When creating a RootAllocator or a child
allocator, a memory limit is provided, and when allocating memory, the limit is checked. Furthermore, when allocating
memory from a child allocator, those allocations are also reflected in all parent allocators. Hence, the RootAllocator
effectively sets the program-wide memory limit, and serves as the master bookkeeper for all memory allocations.
Child allocators are not strictly required, but can help better organize code. For instance, a lower memory limit can
be set for a particular section of code. The child allocator can be closed when that section completes,
at which point it checks that that section didn't leak any memory.
Child allocators can also be named, which makes it easier to tell where an ArrowBuf came from during debugging.
Reference counting
------------------
Because direct memory is expensive to allocate and deallocate, allocators may share direct buffers. To manage shared buffers
deterministically, we use manual reference counting instead of the garbage collector.
This simply means that each buffer has a counter keeping track of the number of references to
the buffer, and the user is responsible for properly incrementing/decrementing the counter as the buffer is used.
In Arrow, each ArrowBuf has an associated `ReferenceManager`_ that tracks the reference count. You can retrieve
it with ArrowBuf.getReferenceManager(). The reference count is updated using `ReferenceManager.release`_ to decrement the count,
and `ReferenceManager.retain`_ to increment it.
Of course, this is tedious and error-prone, so instead of directly working with buffers, we typically use
higher-level APIs like ValueVector. Such classes generally implement Closeable/AutoCloseable and will automatically
decrement the reference count when closed.
Allocators implement AutoCloseable as well. In this case, closing the allocator will check that all buffers
obtained from the allocator are closed. If not, ``close()`` method will raise an exception; this helps track
memory leaks from unclosed buffers.
Reference counting needs to be handled carefully. To ensure that an
independent section of code has fully cleaned up all allocated buffers, use a new child allocator.
Development Guidelines
----------------------
Applications should generally:
* Use the BufferAllocator interface in APIs instead of RootAllocator.
* Create one RootAllocator at the start of the program and explicitly pass it when needed.
* ``close()`` allocators after use (whether they are child allocators or the RootAllocator), either manually or preferably via a try-with-resources statement.
Debugging Memory Leaks/Allocation
---------------------------------
In ``DEBUG`` mode, the allocator and supporting classes will record additional
debug tracking information to better track down memory leaks and issues. To
enable DEBUG mode pass the following system property to the VM when starting
``-Darrow.memory.debug.allocator=true``.
When DEBUG is enabled, a log will be kept of allocations. Configure SLF4J to see these logs (e.g. via Logback/Apache Log4j).
Consider the following example to see how it helps us with the tracking of allocators:
.. code-block:: Java
import org.apache.arrow.memory.ArrowBuf;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
try (BufferAllocator bufferAllocator = new RootAllocator(8 * 1024)) {
ArrowBuf arrowBuf = bufferAllocator.buffer(4 * 1024);
System.out.println(arrowBuf);
}
Without the debug mode enabled, when we close the allocator, we get this:
.. code-block:: shell
11:56:48.944 [main] INFO o.apache.arrow.memory.BaseAllocator - Debug mode disabled.
ArrowBuf[2], address:140508391276544, length:4096
16:28:08.847 [main] ERROR o.apache.arrow.memory.BaseAllocator - Memory was leaked by query. Memory leaked: (4096)
Allocator(ROOT) 0/4096/4096/8192 (res/actual/peak/limit)
Enabling the debug mode, we get more details:
.. code-block:: shell
11:56:48.944 [main] INFO o.apache.arrow.memory.BaseAllocator - Debug mode enabled.
ArrowBuf[2], address:140437894463488, length:4096
Exception in thread "main" java.lang.IllegalStateException: Allocator[ROOT] closed with outstanding buffers allocated (1).
Allocator(ROOT) 0/4096/4096/8192 (res/actual/peak/limit)
child allocators: 0
ledgers: 1
ledger[1] allocator: ROOT), isOwning: , size: , references: 1, life: 261438177096661..0, allocatorManager: [, life: ] holds 1 buffers.
ArrowBuf[2], address:140437894463488, length:4096
reservations: 0
Additionally, in debug mode, `ArrowBuf.print()`_ can be used to obtain a debug string.
This will include information about allocation operations on the buffer with stack traces, such as when/where the buffer was allocated.
.. code-block:: java
import org.apache.arrow.memory.ArrowBuf;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
try (final BufferAllocator allocator = new RootAllocator()) {
try (final ArrowBuf buf = allocator.buffer(1024)) {
final StringBuilder sb = new StringBuilder();
buf.print(sb, /*indent*/ 0);
System.out.println(sb.toString());
}
}
.. code-block:: text
ArrowBuf[2], address:140433199984656, length:1024
event log for: ArrowBuf[2]
675959093395667 create()
at org.apache.arrow.memory.util.HistoricalLog$Event.<init>(HistoricalLog.java:175)
at org.apache.arrow.memory.util.HistoricalLog.recordEvent(HistoricalLog.java:83)
at org.apache.arrow.memory.ArrowBuf.<init>(ArrowBuf.java:96)
at org.apache.arrow.memory.BufferLedger.newArrowBuf(BufferLedger.java:271)
at org.apache.arrow.memory.BaseAllocator.bufferWithoutReservation(BaseAllocator.java:300)
at org.apache.arrow.memory.BaseAllocator.buffer(BaseAllocator.java:276)
at org.apache.arrow.memory.RootAllocator.buffer(RootAllocator.java:29)
at org.apache.arrow.memory.BaseAllocator.buffer(BaseAllocator.java:240)
at org.apache.arrow.memory.RootAllocator.buffer(RootAllocator.java:29)
at REPL.$JShell$14.do_it$($JShell$14.java:10)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:566)
at jdk.jshell.execution.DirectExecutionControl.invoke(DirectExecutionControl.java:209)
at jdk.jshell.execution.RemoteExecutionControl.invoke(RemoteExecutionControl.java:116)
at jdk.jshell.execution.DirectExecutionControl.invoke(DirectExecutionControl.java:119)
at jdk.jshell.execution.ExecutionControlForwarder.processCommand(ExecutionControlForwarder.java:144)
at jdk.jshell.execution.ExecutionControlForwarder.commandLoop(ExecutionControlForwarder.java:262)
at jdk.jshell.execution.Util.forwardExecutionControl(Util.java:76)
at jdk.jshell.execution.Util.forwardExecutionControlAndIO(Util.java:137)
at jdk.jshell.execution.RemoteExecutionControl.main(RemoteExecutionControl.java:70)
The BufferAllocator also provides a ``BufferAllocator.toVerboseString()`` which can be used in
``DEBUG`` mode to get extensive stacktrace information and events associated with various Allocator behaviors.
Finally, enabling the ``TRACE`` logging level will automatically provide this stack trace when the allocator is closed:
.. code-block:: java
// Assumes use of Logback; adjust for Log4j, etc. as appropriate
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import org.apache.arrow.memory.ArrowBuf;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.slf4j.LoggerFactory;
// Set log level to TRACE to get tracebacks
((Logger) LoggerFactory.getLogger("org.apache.arrow")).setLevel(Level.TRACE);
try (final BufferAllocator allocator = new RootAllocator()) {
// Leak buffer
allocator.buffer(1024);
}
.. code-block:: text
| Exception java.lang.IllegalStateException: Allocator[ROOT] closed with outstanding buffers allocated (1).
Allocator(ROOT) 0/1024/1024/9223372036854775807 (res/actual/peak/limit)
child allocators: 0
ledgers: 1
ledger[1] allocator: ROOT), isOwning: , size: , references: 1, life: 712040870231544..0, allocatorManager: [, life: ] holds 1 buffers.
ArrowBuf[2], address:139926571810832, length:1024
event log for: ArrowBuf[2]
712040888650134 create()
at org.apache.arrow.memory.util.StackTrace.<init>(StackTrace.java:34)
at org.apache.arrow.memory.util.HistoricalLog$Event.<init>(HistoricalLog.java:175)
at org.apache.arrow.memory.util.HistoricalLog.recordEvent(HistoricalLog.java:83)
at org.apache.arrow.memory.ArrowBuf.<init>(ArrowBuf.java:96)
at org.apache.arrow.memory.BufferLedger.newArrowBuf(BufferLedger.java:271)
at org.apache.arrow.memory.BaseAllocator.bufferWithoutReservation(BaseAllocator.java:300)
at org.apache.arrow.memory.BaseAllocator.buffer(BaseAllocator.java:276)
at org.apache.arrow.memory.RootAllocator.buffer(RootAllocator.java:29)
at org.apache.arrow.memory.BaseAllocator.buffer(BaseAllocator.java:240)
at org.apache.arrow.memory.RootAllocator.buffer(RootAllocator.java:29)
at REPL.$JShell$18.do_it$($JShell$18.java:13)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)
at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:566)
at jdk.jshell.execution.DirectExecutionControl.invoke(DirectExecutionControl.java:209)
at jdk.jshell.execution.RemoteExecutionControl.invoke(RemoteExecutionControl.java:116)
at jdk.jshell.execution.DirectExecutionControl.invoke(DirectExecutionControl.java:119)
at jdk.jshell.execution.ExecutionControlForwarder.processCommand(ExecutionControlForwarder.java:144)
at jdk.jshell.execution.ExecutionControlForwarder.commandLoop(ExecutionControlForwarder.java:262)
at jdk.jshell.execution.Util.forwardExecutionControl(Util.java:76)
at jdk.jshell.execution.Util.forwardExecutionControlAndIO(Util.java:137)
reservations: 0
| at BaseAllocator.close (BaseAllocator.java:405)
| at RootAllocator.close (RootAllocator.java:29)
| at (#8:1)
Sometimes, explicitly passing allocators around is difficult. For example, it
can be hard to pass around extra state, like an allocator, through layers of
existing application or framework code. A global or singleton allocator instance
can be useful here, though it should not be your first choice.
How this works:
1. Set up a global allocator in a singleton class.
2. Provide methods to create child allocators from the global allocator.
3. Give child allocators proper names to make it easier to figure out where
allocations occurred in case of errors.
4. Ensure that resources are properly closed.
5. Check that the global allocator is empty at some suitable point, such as
right before program shutdown.
6. If it is not empty, review the above allocation bugs.
.. code-block:: java
//1
private static final BufferAllocator allocator = new RootAllocator();
private static final AtomicInteger childNumber = new AtomicInteger(0);
...
//2
public static BufferAllocator getChildAllocator() {
return allocator.newChildAllocator(nextChildName(), 0, Long.MAX_VALUE);
}
...
//3
private static String nextChildName() {
return "Allocator-Child-" + childNumber.incrementAndGet();
}
...
//4: Business code
try (BufferAllocator allocator = GlobalAllocator.getChildAllocator()) {
...
}
...
//5
public static void checkGlobalCleanUpResources() {
...
if (!allocator.getChildAllocators().isEmpty()) {
throw new IllegalStateException(...);
} else if (allocator.getAllocatedMemory() != 0) {
throw new IllegalStateException(...);
}
}
.. _`ArrowBuf`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/ArrowBuf.html
.. _`ArrowBuf.print()`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/ArrowBuf.html#print-java.lang.StringBuilder-int-org.apache.arrow.memory.BaseAllocator.Verbosity-
.. _`BufferAllocator`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/BufferAllocator.html
.. _`BufferLedger`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/BufferLedger.html
.. _`RootAllocator`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/RootAllocator.html
.. _`newChildAllocator`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/RootAllocator.html#newChildAllocator-java.lang.String-org.apache.arrow.memory.AllocationListener-long-long-
.. _`Netty`: https://netty.io/wiki/
.. _`sun.misc.unsafe`: https://web.archive.org/web/20210929024401/http://www.docjar.com/html/api/sun/misc/Unsafe.java.html
.. _`Direct Memory`: https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/ByteBuffer.html
.. _`ReferenceManager`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/ReferenceManager.html
.. _`ReferenceManager.release`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/ReferenceManager.html#release--
.. _`ReferenceManager.retain`: https://arrow.apache.org/docs/java/reference/org/apache/arrow/memory/ReferenceManager.html#retain--
Arrow Memory In-Depth
=====================
Design Principles
-----------------
Arrow’s memory model is based on the following basic concepts:
- Memory can be allocated up to some limit. That limit could be a real
limit (OS/JVM) or a locally imposed limit.
- Allocation operates in two phases: accounting then actual allocation.
Allocation could fail at either point.
- Allocation failure should be recoverable. In all cases, the Allocator
infrastructure should expose memory allocation failures (OS or
internal limit-based) as ``OutOfMemoryException``\ s.
- Any allocator can reserve memory when created. This memory shall be
held such that this allocator will always be able to allocate that
amount of memory.
- A particular application component should work to use a local
allocator to understand local memory usage and better debug memory
leaks.
- The same physical memory can be shared by multiple allocators and the
allocator must provide an accounting paradigm for this purpose.
Reserving Memory
----------------
Arrow provides two different ways to reserve memory:
- BufferAllocator accounting reservations: When a new allocator (other
than the ``RootAllocator``) is initialized, it can set aside memory
that it will keep locally for its lifetime. This is memory that will
never be released back to its parent allocator until the allocator is
closed.
- ``AllocationReservation`` via BufferAllocator.newReservation():
Allows a short-term preallocation strategy so that a particular
subsystem can ensure future memory is available to support a
particular request.
Reference Counting Details
--------------------------
Typically, the ReferenceManager implementation used is an instance of `BufferLedger`_.
A BufferLedger is a ReferenceManager that also maintains the relationship between an ``AllocationManager``,
a ``BufferAllocator`` and one or more individual ``ArrowBuf``\ s
All ArrowBufs (direct or sliced) related to a single BufferLedger/BufferAllocator combination
share the same reference count and either all will be valid or all will be invalid.
For simplicity of accounting, we treat that memory as being used by one
of the BufferAllocators associated with the memory. When that allocator
releases its claim on that memory, the memory ownership is then moved to
another BufferLedger belonging to the same AllocationManager.
Allocation Details
------------------
There are several Allocator types in Arrow Java:
- ``BufferAllocator`` - The public interface application users should be leveraging
- ``BaseAllocator`` - The base implementation of memory allocation, contains the meat of the Arrow allocator implementation
- ``RootAllocator`` - The root allocator. Typically only one created for a JVM. It serves as the parent/ancestor for child allocators
- ``ChildAllocator`` - A child allocator that derives from the root allocator
Many BufferAllocators can reference the same piece of physical memory at the same
time. It is the AllocationManager’s responsibility to ensure that in this situation,
all memory is accurately accounted for from the Root’s perspective
and also to ensure that the memory is correctly released once all
BufferAllocators have stopped using that memory.
For simplicity of accounting, we treat that memory as being used by one
of the BufferAllocators associated with the memory. When that allocator
releases its claim on that memory, the memory ownership is then moved to
another BufferLedger belonging to the same AllocationManager. Note that
because a ArrowBuf.release() is what actually causes memory ownership
transfer to occur, we always proceed with ownership transfer (even if
that violates an allocator limit). It is the responsibility of the
application owning a particular allocator to frequently confirm whether
the allocator is over its memory limit (BufferAllocator.isOverLimit())
and if so, attempt to aggressively release memory to ameliorate the
situation.
Object Hierarchy
----------------
There are two main ways that someone can look at the object hierarchy
for Arrow’s memory management scheme. The first is a memory based
perspective as below:
Memory Perspective
~~~~~~~~~~~~~~~~~~
.. code-block:: none
+ AllocationManager
|
|-- UnsignedDirectLittleEndian (One per AllocationManager)
|
|-+ BufferLedger 1 ==> Allocator A (owning)
| ` - ArrowBuf 1
|-+ BufferLedger 2 ==> Allocator B (non-owning)
| ` - ArrowBuf 2
|-+ BufferLedger 3 ==> Allocator C (non-owning)
| - ArrowBuf 3
| - ArrowBuf 4
` - ArrowBuf 5
In this picture, a piece of memory is owned by an allocator manager. An
allocator manager is responsible for that piece of memory no matter
which allocator(s) it is working with. An allocator manager will have
relationships with a piece of raw memory (via its reference to
UnsignedDirectLittleEndian) as well as references to each
BufferAllocator it has a relationship to.
Allocator Perspective
~~~~~~~~~~~~~~~~~~~~~
.. code-block:: none
+ RootAllocator
|-+ ChildAllocator 1
| | - ChildAllocator 1.1
| ` ...
|
|-+ ChildAllocator 2
|-+ ChildAllocator 3
| |
| |-+ BufferLedger 1 ==> AllocationManager 1 (owning) ==> UDLE
| | `- ArrowBuf 1
| `-+ BufferLedger 2 ==> AllocationManager 2 (non-owning)==> UDLE
| `- ArrowBuf 2
|
|-+ BufferLedger 3 ==> AllocationManager 1 (non-owning)==> UDLE
| ` - ArrowBuf 3
|-+ BufferLedger 4 ==> AllocationManager 2 (owning) ==> UDLE
| - ArrowBuf 4
| - ArrowBuf 5
` - ArrowBuf 6
In this picture, a RootAllocator owns three ChildAllocators. The first
ChildAllocator (ChildAllocator 1) owns a subsequent ChildAllocator.
ChildAllocator has two BufferLedgers/AllocationManager references.
Coincidentally, each of these AllocationManager’s is also associated
with the RootAllocator. In this case, one of the these
AllocationManagers is owned by ChildAllocator 3 (AllocationManager 1)
while the other AllocationManager (AllocationManager 2) is
owned/accounted for by the RootAllocator. Note that in this scenario,
ArrowBuf 1 is sharing the underlying memory as ArrowBuf 3. However the
subset of that memory (e.g. through slicing) might be different. Also
note that ArrowBuf 2 and ArrowBuf 4, 5 and 6 are also sharing the same
underlying memory. Also note that ArrowBuf 4, 5 and 6 all share the same
reference count and fate.