How Android’s ART Runtime Manages Heap Memory: A Deep Dive into GC Structures
This article explains why reading the ART virtual‑machine source is valuable, compares Dalvik and ART, outlines the GC source hierarchy, details the Space class architecture—including accounting, allocator, collector, and specific spaces like LargeObjectSpace, BumpPointerSpace, and RegionSpace—then analyzes heap construction, PreZygoteFork processing, memory‑map layout, and the full Java‑object allocation flow in ART.
Why Read the ART VM Source
Reading the ART runtime source gives direct insight into how Android allocates heap memory, how the VM structures its heap spaces, and which APIs can be used for performance monitoring.
Dalvik vs. ART
Dalvik and ART are independent VM implementations that both execute the same bytecode. Dalvik builds libdvm.so, while ART builds libart.so. Devices running Android 5.0+ use ART exclusively, which is the focus of this analysis.
GC Source Structure Overview
accounting – data structures for tracking object modifications (e.g., bitmap.h/.cc, card_table.h/.cc).
allocator – memory‑allocation algorithms; currently dlmalloc and rosalloc.
collector – garbage‑collector implementations (Mark‑Sweep, Copy, etc.). ART does not use reference counting.
space – concrete memory‑resource providers for the heap (e.g., space.h/.cc, region_space.h/.cc).
heap – high‑level heap class that orchestrates allocation and GC.
Space Class Hierarchy
In ART, Java‑level heap memory is allocated through the Space abstraction. Each Space represents a memory resource container, either continuous or discontinuous.
Key Sub‑systems
accounting – auxiliary structures for GC analysis.
allocator – DlMallocSpace and RosAllocSpace implement the two allocation algorithms.
collector – provides MarkSweepCollection and CopyCollection.
space – concrete implementations such as ContinuousSpace, DiscontinuousSpace, MallocSpace, LargeObjectSpace, etc.
Space Base Class
The base Space defines attributes like name and GC retention policy. The retention policy determines when memory in the space is eligible for collection: kGcRetentionPolicyNeverCollect – never collect. kGcRetentionPolicyAlwaysCollect – collect on every GC trigger. kGcRetentionPolicyFullCollect – collect only during a full GC.
Continuous vs. Discontinuous Spaces
Continuous spaces provide a single contiguous memory region and expose Begin(), End(), and Limit(). Discontinuous spaces consist of non‑contiguous regions and rely on mark‑sweep for reclamation, exposing getLiveBitmap() and getMarkBitmap() for tracking object liveness.
Specific Space Implementations
MemMapSpace
MemMapSpaceuses the MemMap helper (a thin wrapper around mmap) to request memory from the OS.
ImageSpace
ImageSpacemaps ART image files (e.g., boot.art, boot.oat) via file‑backed mmap for fast loading of boot classes.
MallocSpace, DlMallocSpace, RosAllocSpace
MallocSpaceis the abstract base for object allocation. DlMallocSpace implements Doug Lea’s dlmalloc algorithm; RosAllocSpace implements Android’s rosalloc algorithm. Modern ART builds primarily use rosalloc, with dlmalloc retained for legacy paths.
BumpPointerSpace
A simple continuous allocator where a pointer monotonically advances; it cannot free individual objects, only the whole space.
namespace art { namespace gc { namespace space {
BumpPointerSpace* BumpPointerSpace::Create(const std::string& name, size_t capacity) {
capacity = RoundUp(capacity, kPageSize);
std::string error_msg;
MemMap mem_map = MemMap::MapAnonymous(name.c_str(), capacity,
PROT_READ | PROT_WRITE, true, &error_msg);
if (!mem_map.IsValid()) {
LOG(ERROR) << "Failed to allocate pages for alloc space (" << name << ") of size "
<< PrettySize(capacity) << " with message " << error_msg;
return nullptr;
}
return new BumpPointerSpace(name, std::move(mem_map));
}
BumpPointerSpace::BumpPointerSpace(const std::string& name, MemMap&& mem_map)
: ContinuousMemMapAllocSpace(name, std::move(mem_map), mem_map.Begin(),
mem_map.Begin(), mem_map.End(),
kGcRetentionPolicyAlwaysCollect),
growth_end_(mem_map_.End()), objects_allocated_(0), bytes_allocated_(0),
block_lock_("Block lock", kBumpPointerSpaceBlockLock),
main_block_size_(0), num_blocks_(0) {}
}}}RegionSpace
Divides memory into fixed‑size 256 KB regions. Allocation occurs within a region; if a region fills, a new one is allocated.
static constexpr size_t kRegionSize = 256 * KB;
RegionSpace::RegionSpace(const std::string& name, MemMap&& mem_map, bool use_generational_cc)
: ContinuousMemMapAllocSpace(name, std::move(mem_map), mem_map.Begin(), mem_map.End(),
mem_map.End(), kGcRetentionPolicyAlwaysCollect),
region_lock_("Region lock", kRegionSpaceRegionLock),
use_generational_cc_(use_generational_cc),
num_regions_(mem_map_.Size() / kRegionSize),
regions_(new Region[num_regions_]) {
uint8_t* region_addr = mem_map_.Begin();
for (size_t i = 0; i < num_regions_; ++i, region_addr += kRegionSize) {
regions_[i].Init(i, region_addr, region_addr + kRegionSize);
}
mark_bitmap_ = accounting::ContinuousSpaceBitmap::Create(
"region space live bitmap", Begin(), Capacity());
Protect();
}LargeObjectSpace
Handles objects larger than three pages. Two concrete subclasses exist:
LargeObjectMapSpace – allocates each large object with a dedicated mmap and keeps a map from object pointer to its MemMap.
FreeListSpace – uses a free‑list allocator; on x86 the default is FreeListSpace, on ARM it is LargeObjectMapSpace.
<span>mirror::Object* LargeObjectMapSpace::Alloc(Thread* self, size_t num_bytes,
size_t* bytes_allocated,
size_t* usable_size,
size_t* bytes_tl_bulk_allocated) {</span>
std::string error_msg;
MemMap mem_map = MemMap::MapAnonymous("large object space allocation",
num_bytes, PROT_READ | PROT_WRITE,
true, &error_msg);
if (!mem_map.IsValid()) {
LOG(WARNING) << "Large object allocation failed: " << error_msg;
return nullptr;
}
mirror::Object* const obj = reinterpret_cast<mirror::Object*>(mem_map.Begin());
size_t allocation_size = mem_map.BaseSize();
MutexLock mu(self, lock_);
large_objects_.Put(obj, LargeObject{std::move(mem_map), false});
*bytes_allocated = allocation_size;
if (usable_size != nullptr) *usable_size = allocation_size;
*bytes_tl_bulk_allocated = allocation_size;
return obj;
}Heap Construction Analysis
The Heap constructor creates the various spaces based on runtime parameters, GC type, and whether the process is the Zygote.
Loads boot image(s) via ImageSpace::LoadBootImage and records their address range.
Creates a separate non‑moving space (used for classes, methods, etc.) when needed.
Depending on the foreground collector, it creates either a RegionSpace (for copying collectors) or one or two BumpPointerSpace instances (for mark‑sweep collectors).
If rosalloc is enabled, CreateMainMallocSpace builds a RosAllocSpace; otherwise a DlMallocSpace is used.
Initializes the large‑object space according to the configured LargeObjectSpaceType.
Sets up bookkeeping structures: card table, read‑barrier table, mod‑union tables, remembered sets, and various GC‑related locks.
Key default parameters (from Heap.h) include:
static constexpr size_t kDefaultStartingSize = kPageSize;
static constexpr size_t kDefaultInitialSize = 2 * MB;
static constexpr size_t kDefaultMaximumSize = 256 * MB;
static constexpr size_t kDefaultNonMovingSpaceCapacity = 64 * MB;
static constexpr size_t kDefaultTLABSize = 32 * KB;
static constexpr double kDefaultTargetUtilization = 0.75;
static constexpr double kDefaultHeapGrowthMultiplier = 2.0;PreZygoteFork
When the Zygote process forks, Heap::PreZygoteFork compacts the non‑moving space, creates a ZygoteSpace (a BumpPointerSpace), and splits the remaining memory into a new non‑moving space. It also marks surviving large objects as belonging to the Zygote space and updates mod‑union tables.
Memory‑Map Layout (Space Portion)
Reading /proc/${pid}/maps on Android shows the layout of the various ART spaces, for example: dalvik-main space – the primary allocation space.
Boot image mappings at 0x70000000 plus a random offset. dalvik-zygote space and dalvik-non moving space created during PreZygoteFork.
Various ashmem regions for card tables, allocation stacks, and other GC data structures.
Java Object Allocation Flow
The high‑level entry point is Heap::AllocObject, which forwards to AllocObjectWithAllocator. The flow includes:
Pre‑allocation hook via AllocationListener::PreObjectAllocated (if instrumentation is enabled).
Large‑object check – if the object exceeds the large‑object threshold, AllocLargeObject is invoked, which ultimately calls AllocObjectWithAllocator with kAllocatorTypeLOS.
If the current thread has a TLAB and the request fits, allocation occurs directly from the TLAB.
Otherwise, TryToAllocate attempts allocation from the appropriate space based on the current allocator (e.g., rosalloc, dlmalloc, bump pointer, region).
On failure, AllocateInternalWithGc triggers a GC cycle and retries.
If allocation still fails without an exception, the method restarts with the default instrumented allocator.
During allocation the object's class pointer is set, read‑barrier state is asserted (when enabled), and optional write barriers are applied for non‑movable spaces.
Conclusion
This analysis covered the ART runtime’s heap architecture, the Space class hierarchy, the creation of various allocation spaces, the pre‑fork compaction of Zygote memory, and the detailed steps the VM takes to allocate a Java object. Future work will examine the collector implementations themselves and discuss how developers can monitor ART memory behavior using JVMTI, native hooks, or other instrumentation techniques.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
DeWu Technology
A platform for sharing and discussing tech knowledge, guiding you toward the cloud of technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
