Dart 51 🎯 Dart Memory Model: Garbage Collection (Scavenger & Mark-Sweep Algorithms)
Dart’s memory model is built around a single insight: most objects die young. In a typical application, the majority of allocated objects become unreachable almost immediately, while a small minority survive to become long-lived. A garbage collector that treats all objects equally wastes time repeatedly examining the long-lived ones. A collector that exploits the age distribution, by separating young objects from old ones and collecting them with different strategies, achieves far better throughput. Dart’s garbage collector does exactly this. It uses a two-generation heap: a young generation collected by a copying scavenger, and an old generation collected by mark-sweep with concurrent marking. Understanding both algorithms is what lets a developer reason about pause times, allocation rates, and why Flutter applications can allocate thousands of short-lived widgets without jank.
Key point: Dart’s garbage collector is generational. The young generation is collected frequently, cheaply, and with a brief pause using the Cheney scavenger. The old generation is collected rarely, more expensively, and with concurrent marking to minimize the pause. The two algorithms complement each other, and neither alone would be sufficient.
Why a generational collector exists
The weak generational hypothesis. The hypothesis states that most objects die young. Empirical measurements across object-oriented systems confirm it. If this is true, then repeatedly scanning the entire heap for garbage is wasteful, because the vast majority of the heap consists of objects that are already known to be reachable. A collector that focuses its effort on the young objects, where the garbage is concentrated, will collect more garbage per unit of work.
The pause-time problem. A garbage collector must stop the application to safely move objects or reclaim memory. The length of this pause determines whether the user notices a stutter. A full-heap collection of a large application can pause for hundreds of milliseconds, which is unacceptable for a UI framework. By splitting the heap into generations, the collector can perform many short pauses on the small young generation and only occasionally perform a longer pause on the old generation.
The allocation-rate problem. Some workloads allocate extremely rapidly. A Flutter build pass can create hundreds of widgets in a frame, most of which are discarded by the next frame. A collector optimized for this pattern must make allocation cheap and reclamation fast. The young generation uses bump-pointer allocation, which is effectively a pointer increment, and the scavenger’s copying strategy makes reclamation proportional to the number of surviving objects, not the total number of allocated objects.
The floating-garbage problem. A single-heap collector would collect all unreachable objects in one pass. A generational collector may leave some garbage uncollected, because an unreachable old object that is only referenced by an unreachable young object will not be collected until the young object is collected first. This is floating garbage, and it is the price of generational collection. The collector’s growth policy must ensure that old-space collections are interleaved with young-space collections to prevent floating garbage from accumulating without bound.
a. The young generation and the scavenger
The young generation is where every object is first allocated. It is designed for rapid allocation and cheap reclamation. The scavenger collects it frequently, and the pause is brief.
Semi-space layout
The young generation is divided into two equal halves called semi-spaces: a from-space and a to-space. At any moment, only one is active. New objects are allocated in the active semi-space. When it fills, the scavenger runs .
Young generation:
┌──────────────────────────────────────────────┐
│ From-space (active) │ To-space (inactive) │
│ New allocations here │ Waiting for GC │
└──────────────────────────────────────────────┘
The semi-spaces are sized at 8 MB on 32-bit platforms and 16 MB on 64-bit platforms by default .
Bump-pointer allocation
Allocation in the young generation is a pointer increment. A thread-local allocation buffer (TLAB) gives the current thread a region of the active semi-space. Allocating an object means moving the bump pointer forward by the object’s size and returning the old pointer value. There is no free-list search, no metadata lookup, and no locking .
Bump pointer allocation:
┌──────────────────────────────────────────────┐
│ Allocated objects │ Free space │
│ ↑ │
│ bump pointer │
└──────────────────────────────────────────────┘
Allocate: return bump pointer, advance by size.
This makes allocation essentially free compared to a free-list-based allocator.
The Cheney scavenger
When the active semi-space fills, the scavenger runs. It uses the Cheney algorithm, a breadth-first copying collector .
The algorithm works in four steps:
- Swap the semi-spaces. The from-space becomes inactive, and the to-space becomes active. New allocations go to the new active half.
- Copy roots. The collector visits all root references: stack variables, global variables, and the remembered set (old objects that point to young objects). Each young object referenced by a root is copied to the to-space. The old location in from-space is overwritten with a forwarding pointer that points to the new location .
- Scan and copy. The collector scans the to-space from the beginning. For each object it finds, it examines the object’s fields. Any reference to a young object in from-space is followed: if the object has not been copied yet, it is copied to the to-space and the reference is updated to point to the new location. If it has already been copied, the reference is updated using the forwarding pointer. This process continues until the scan pointer reaches the end of the copied objects .
- Reclaim from-space. Once all live objects have been copied, the entire from-space is released. The dead objects are simply left behind, and the from-space becomes the new inactive half for the next cycle.
Cheney scavenger:
┌──────────────────────────────────────────────┐
│ From-space (dead) │ To-space (live) │
│ All objects abandoned│ Live objects copied │
└──────────────────────────────────────────────┘
After GC: swap roles.
The cost of the scavenger is proportional to the number of surviving objects, not the number of allocated objects. If a frame allocates a thousand widgets and nine hundred die, the scavenger copies only the hundred survivors. The nine hundred dead objects are reclaimed for free when the from-space is abandoned .
Parallel scavenge
Dart starts two scavenger workers by default . Each worker processes a portion of the root set and the remembered set. When a worker copies an object, it allocates from a worker-local bump region and processes that object itself. Promoted objects are added to a shared work list with work stealing. Forwarding pointers are installed with a compare-and-swap, so a losing worker un-allocates its copy and uses the winner’s forwarding pointer .
Promotion
An object that survives a certain number of scavenges is promoted to the old generation. Promotion happens during the scavenger’s copy step: instead of copying the object to the to-space, the scavenger copies it to the old generation’s page space . Once promoted, the object is managed by the mark-sweep collector. The promotion threshold is tuned so that objects that are likely to survive for a long time are moved out of the young generation, reducing the scavenger’s work on future cycles.
The remembered set
A young object can be referenced by an old object. If the scavenger only scanned roots and the young generation, it would miss these references. The remembered set solves this. Whenever a store creates a pointer from an old object to a young object, the write barrier adds the old object to the remembered set .
Write barrier (generational):
if (container is old && container is not remembered && value is young):
container.setRemembered()
addToRememberedSet(container)
The scavenger treats the remembered set as part of the root set. Old objects in the remembered set are scanned, and any young objects they reference are copied. This ensures that young objects reachable only from old objects are not incorrectly collected .
b. The old generation and mark-sweep
Objects that survive young-generation collection are promoted to the old generation. This generation is larger, holds longer-lived objects, and is collected with mark-sweep. Because the old generation is large, its collection is more expensive, so Dart uses concurrent marking to reduce the pause.
The mark-sweep algorithm
Mark-sweep has two phases. In the marking phase, the collector traverses the object graph from the roots, marking every reachable object. In the sweeping phase, it walks the heap and reclaims every object that was not marked .
Mark phase:
Start from roots.
For each reachable object, set mark bit.
Continue until all reachable objects are marked.
Sweep phase:
Walk the heap.
Unmarked objects: return memory to free list.
Marked objects: clear mark bit.
Every object has a mark bit in its header. At the start of a collection, all mark bits are clear. The marker visits roots and marks reachable old-space objects. New-space objects are treated as roots and visited during a safepoint, avoiding the need to synchronize with the concurrent marker on every young object .
Concurrent marking
The pause during marking is the most disruptive part of old-generation collection. Dart reduces this pause by running most of the marking work concurrently with the application .
While the mutator runs, the marker traverses the object graph in the background. The danger is a race: the mutator writes a pointer to an unmarked object into an already-marked object. If the marker has already visited the marked object and does not revisit it, the unmarked target will be missed and incorrectly collected .
The write barrier prevents this. When the mutator stores a pointer from an old object to an unmarked old object while marking is in progress, the barrier marks the target and adds it to the marking worklist .
Write barrier (incremental):
if (container is old && value is old && value is not marked && marking):
mark value
add value to marking worklist
The barrier checks are combined with the generational barrier using a shift-and-mask on the header bits, so the common case is a single test and branch .
Tri-color abstraction
The concurrent marker uses the tri-color abstraction. Objects are white (unvisited), grey (visited but children not yet scanned), or black (visited and children scanned). The marker starts with roots in the grey set. It removes a grey object, scans its children, and marks them grey. When all children are processed, the object becomes black. The collection is complete when no grey objects remain .
The write barrier ensures the invariant that no black object points to a white object. If a black object is modified to point to a white object, the barrier marks the white object grey, restoring the invariant.
Sweeping
After marking completes, the sweeper reclaims unmarked objects. The sweep is concurrent with the mutator, using a separate sweeper thread or threads. The sweeper walks each page, and for each unmarked object, adds its memory to a free list .
Sweep:
For each page:
For each object:
if not marked: add to free list
else: clear mark bit
If an entire page contains only unmarked objects, the page is released to the operating system .
Compaction
Dart’s old generation includes a sliding compactor, used during idle intervals. Compaction moves live objects to eliminate fragmentation and reduce the number of pages needed. The forwarding table is compressed by dividing the heap into blocks and recording target addresses per block, so lookup is constant time . Compaction is not run on every collection; it is reserved for idle periods to avoid adding to pause times.
c. Interplay between generations
The two generations are not independent. The scavenger and the mark-sweep collector interact in ways that affect performance and correctness.
New-space objects as roots
During old-generation marking, new-space objects are treated as roots. The marker does not traverse into the young generation, because doing so would require synchronizing with the scavenger. Instead, all young objects are assumed reachable during the mark phase. This makes the two collectors more independent and avoids data races .
The trade-off is that a single old-generation collection cannot collect all garbage. An unreachable old object that is referenced only by an unreachable young object remains uncollected until the scavenger runs and collects the young object. This is floating garbage, and the growth policy must ensure that scavenges are interleaved with old-generation collections .
Promotion and the remembered set
When the scavenger promotes an object, it must check whether the promoted object references any young objects. If it does, the promoted object is added to the remembered set, because it is now an old object that may point to young objects . The promotion process updates the write barrier state for the promoted object.
Safepoints
The scavenger and the marker synchronize at safepoints. During a scavenge, the mutator is stopped, and all threads reach a safepoint before the collection begins. New-space objects and roots are only visited at safepoints, which establishes the synchronization needed for the concurrent marker to safely treat new-space as roots .
Scheduling
The Dart VM coordinates with the Flutter engine to schedule collections during idle periods. When the engine detects that the application is idle and no user interaction is occurring, it alerts the garbage collector. The collector uses these windows to run collection phases, including compaction, without impacting frame rendering .
Complete Example Session
// ============================================
// PART 1: ALLOCATION IN THE YOUNG GENERATION
// ============================================
// Every object starts in the young generation.
void allocate() {
final list = <int>[]; // Bump-pointer allocated in young space.
final widget = Text('Hello'); // Bump-pointer allocated in young space.
final map = <String, int>{}; // Bump-pointer allocated in young space.
}
// Allocation is a pointer increment in the TLAB.
// No free-list search. No locking.
// ============================================
// PART 2: THE SCAVENGER RUNS
// ============================================
// When the active semi-space fills, the scavenger runs.
// Live objects are copied to the to-space.
// Dead objects are abandoned in from-space.
// Before scavenge:
// From-space: [list, widget, map, dead1, dead2, dead3]
// To-space: empty
// After scavenge:
// From-space: abandoned
// To-space: [list, widget, map] (if they are still reachable)
// The dead objects are reclaimed for free.
// ============================================
// PART 3: PROMOTION TO OLD GENERATION
// ============================================
// An object that survives multiple scavenges is promoted.
void longLived() {
final cache = <String, Object>{}; // Survives many scavenges.
// Eventually promoted to old space.
}
// Promotion happens during the scavenger's copy step.
// The object is copied to old-space instead of to-space.
// ============================================
// PART 4: THE REMEMBERED SET
// ============================================
// An old object references a young object.
final oldObject = OldObject();
oldObject.child = YoungObject();
// The write barrier fires:
// container = oldObject (old)
// value = YoungObject (young)
// container is added to the remembered set.
// During the next scavenge, oldObject is scanned as a root,
// and YoungObject is copied if still reachable.
// ============================================
// PART 5: MARK-SWEEP ON OLD GENERATION
// ============================================
// When old space fills, mark-sweep runs.
// Mark phase: traverse from roots, mark reachable objects.
// Sweep phase: reclaim unmarked objects.
// Concurrent marking:
// The marker runs while the mutator continues.
// The write barrier marks new targets.
// ============================================
// PART 6: THE WRITE BARRIER IN ACTION
// ============================================
// During concurrent marking, the mutator stores:
markedObject.field = unmarkedObject;
// The incremental write barrier fires:
// container = markedObject (old, already marked)
// value = unmarkedObject (old, not marked)
// marking is in progress
// Barrier action:
// unmarkedObject.setMarkBit()
// markingWorklist.add(unmarkedObject)
// The unmarked object is now preserved.
// ============================================
// PART 7: FLOATING GARBAGE
// ============================================
// An unreachable old object referenced only by an
// unreachable young object is floating garbage.
final oldGarbage = OldObject();
final youngRef = YoungObject()..ref = oldGarbage;
// oldGarbage becomes unreachable when youngRef becomes unreachable.
// The old-space collector treats youngRef as a root.
// So oldGarbage appears reachable and is not collected.
// It remains floating garbage until a scavenge collects youngRef.
// ============================================
// PART 8: SAFEPOINTS AND SYNCHRONIZATION
// ============================================
// The marker cannot safely read new-space without synchronization.
// So new-space is treated as roots during marking.
// Roots and new-space are only visited at safepoints.
// A safepoint is a point where all threads are known to be paused.
// ============================================
// PART 9: COMPACTION DURING IDLE
// ============================================
// When the application is idle, the compactor runs.
// It moves live objects to eliminate fragmentation.
// The forwarding table maps old addresses to new addresses.
// This runs during idle intervals, not during frames.
// ============================================
// PART 10: THE COMPLETE PICTURE
// ============================================
// 1. Objects are allocated in young space (bump pointer).
// 2. When young space fills, the scavenger copies survivors.
// 3. Objects that survive many scavenges are promoted.
// 4. Old space fills, triggering mark-sweep.
// 5. Marking runs concurrently, with write barriers.
// 6. Sweeping runs concurrently, reclaiming dead objects.
// 7. Compaction runs during idle periods.
// 8. The cycle repeats.
The ten parts followed an object from allocation through scavenging, promotion, remembered-set handling, concurrent marking, write-barrier interaction, floating garbage, safepoints, compaction, and the complete cycle. The same allocation patterns ran through every part, showing how the two generations divide the work.
Quick Reference
Generational Collector Summary
| Generation | Collector | Pause | Strategy |
|---|---|---|---|
| Young | Scavenger | Brief | Cheney copying |
| Old | Mark-Sweep | Longer | Concurrent marking |
Scavenger Algorithm Steps
| Step | Action |
|---|---|
| 1 | Swap from-space and to-space |
| 2 | Copy roots to to-space |
| 3 | Scan copied objects, copy referenced young objects |
| 4 | Reclaim from-space entirely |
Mark-Sweep Phases
| Phase | Action | Concurrency |
|---|---|---|
| Mark | Traverse from roots, set mark bits | Concurrent |
| Sweep | Reclaim unmarked objects | Concurrent |
Write Barrier Types
| Barrier | Condition | Action |
|---|---|---|
| Generational | Old → Young | Add old to remembered set |
| Incremental | Old → Unmarked Old | Mark target, add to worklist |
Young Generation Sizes
| Platform | Semi-space Size | Max Object |
|---|---|---|
| 32-bit | 8 MB | 256 KB |
| 64-bit | 16 MB | 256 KB |
Best Practices
✅ Do This:
// Let the garbage collector handle short-lived objects.
final widget = Text('Hello'); // ✅
// Use local variables for short-lived objects.
void build() {
final list = <int>[]; // ✅ Dies young
}
// Rely on the scavenger for rapid allocation.
// Flutter creates thousands of widgets per frame.
// The scavenger handles them efficiently. // ✅
// Keep Isolate heaps independent.
// Each Isolate has its own GC. // ✅
// Trust the scheduling with the Flutter engine.
// GC runs during idle periods. // ✅
❌ Don’t Do This:
// Don't cache short-lived objects in long-lived state.
class BadWidget extends StatefulWidget {
final widget = Text('Hello'); // ❌ Keeps short-lived object alive
}
// Don't hold references to disposed objects.
disposedObject = null; // ❌ Must be explicitly nulled if retained elsewhere
// Don't assume GC prevents all leaks.
// Unreachable objects are collected.
// Reachable-but-unneeded objects are not. // ❌
// Don't fear the scavenger.
// It is optimized for rapid allocation. // ❌
// Don't manually manage memory unless profiling shows a problem.
// Let the generational collector do its job. // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Memory growth | Unneeded objects remain reachable | Remove references to unneeded objects |
| Jank from GC | Old-space collection during frame | Trust scheduling; reduce allocations |
| Floating garbage | Old object referenced only by young | Scavenger interleaved with mark-sweep |
| Remembered set overflow | Many old→young references | Detected and handled by full GC |
| Promotion too early | Object survives threshold | Tune if profiling shows issue |
| Compaction pause | Idle compaction runs | Scheduling avoids frame impact |
| Leaks from closures | Closure captures large object | Break references when done |
Real-World Examples
1. Short-Lived Widget Allocation
// Flutter creates thousands of widgets per frame.
// The scavenger collects them efficiently.
Text('Hello') // Allocated in young space
2. State Object Held in State
// BAD: Short-lived widget cached in State.
// Kept alive by the State object.
// ❌ Don't do this.
3. Isolate Heap Independence
// Each Isolate has its own heap and GC.
// GC in one Isolate does not affect another.
final isolate = await Isolate.spawn(worker);
4. Closure Capture
// BAD: Closure captures large object.
final handler = () => print(myHugeObject.name);
// myHugeObject kept alive by handler.
// ✅ Null the reference when done.
5. Mark-Sweep on Old Space
// Long-lived cache eventually promoted to old space.
final cache = <String, Object>{};
// Old space collected by mark-sweep.
6. Write Barrier
// Old object stores reference to young object.
oldObject.child = youngObject;
// Write barrier adds oldObject to remembered set.
7. Safepoint Synchronization
// Marker treats new-space as roots.
// New-space visited at safepoints.
// No per-object synchronization needed.
8. Compaction During Idle
// When app is idle, compactor runs.
// Reduces fragmentation.
// No frame impact.
9. Bump-Pointer Allocation
// Allocation is a pointer increment.
final obj = Object(); // TLAB bump pointer.
10. Promotion Threshold
// Object survives N scavenges → promoted.
// N is tuned by the VM.
// No manual control needed.
Visual
The Generational Heap
┌────────────────────────────────────────────────────────────┐
│ DART GENERATIONAL HEAP │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ YOUNG GENERATION │ │
│ │ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ From-space │ │ To-space │ │ │
│ │ │ (active) │ │ (inactive) │ │ │
│ │ │ New objects │ │ Waiting │ │ │
│ │ └───────────────┘ └───────────────┘ │ │
│ │ Collected by: Scavenger (Cheney copying) │ │
│ │ Pause: Brief │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ │ Promotion │
│ v │
│ ┌──────────────────────────────────────────────────┐ │
│ │ OLD GENERATION │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ Pages managed by PageSpace │ │ │
│ │ │ Free lists for allocation │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ Collected by: Mark-Sweep (concurrent) │ │
│ │ Pause: Longer but less frequent │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Each Isolate has its own heap and its own GC. │
│ │
└────────────────────────────────────────────────────────────┘
The Scavenger Cycle
┌────────────────────────────────────────────────────────────┐
│ SCAVENGER CYCLE │
│ │
│ BEFORE │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ From-space │ │ To-space │ │
│ │ [A][B][C][D] │ │ empty │ │
│ │ (active) │ │ │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ STEP 1: Swap │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ From-space │ │ To-space │ │
│ │ [A][B][C][D] │ │ (active) │ │
│ │ (inactive) │ │ │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ STEP 2: Copy roots (A and C are reachable) │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ From-space │ │ To-space │ │
│ │ [A][B][C][D] │ │ [A][C] │ │
│ │ │ │ scan → │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ STEP 3: Scan and copy references │
│ A references B → B copied │
│ C references D → D copied │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ From-space │ │ To-space │ │
│ │ [dead] │ │ [A][C][B][D] │ │
│ │ │ │ scan → done │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ STEP 4: Abandon from-space │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Released │ │ [A][C][B][D] │ │
│ │ │ │ (now from) │ │
│ └───────────────┘ └───────────────┘ │
│ │
│ Cost proportional to survivors, not allocations. │
│ │
└────────────────────────────────────────────────────────────┘
Mark-Sweep with Concurrent Marking
┌────────────────────────────────────────────────────────────┐
│ CONCURRENT MARK-SWEEP │
│ │
│ MARK PHASE (concurrent with mutator) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Marker thread: Mutator thread: │ │
│ │ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Traverse │ │ Allocate │ │ │
│ │ │ object │ │ objects │ │ │
│ │ │ graph │ │ Modify │ │ │
│ │ │ Mark bits │ │ pointers │ │ │
│ │ └────────────┘ └────────────┘ │ │
│ │ │ │ │ │
│ │ │ Write barrier │ │ │
│ │ │◀─────────────────────┘ │ │
│ │ │ Mark new targets │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ SWEEP PHASE (concurrent) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Sweeper thread: │ │
│ │ Walk pages │ │
│ │ Unmarked → free list │ │
│ │ Marked → clear bit │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ Pause only at start and end of marking. │
│ │
└────────────────────────────────────────────────────────────┘
Write Barrier Logic
┌────────────────────────────────────────────────────────────┐
│ WRITE BARRIER DECISION │
│ │
│ Store: container.slot = value │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Is container old? │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ YES NO │
│ │ │ │
│ v v │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Is value young? │ │ No barrier needed. │ │
│ └──────────────────────┘ │ New objects are │ │
│ │ │ roots. │ │
│ ┌────┴────┐ └──────────────────────┘ │
│ │ │ │
│ YES NO │
│ │ │ │
│ v v │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ GENERATIONAL │ │ Is marking in progress? │ │
│ │ Add container│ └──────────────────────────────┘ │
│ │ to remembered│ │ │
│ │ set. │ ┌────┴────┐ │
│ └──────────────┘ YES NO │
│ │ │ │
│ v v │
│ ┌──────────┐ ┌──────────┐ │
│ │ Is value │ │ No │ │
│ │ marked? │ │ barrier. │ │
│ └──────────┘ └──────────┘ │
│ │ │
│ ┌────┴────┐ │
│ YES NO │
│ │ │ │
│ v v │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ No │ │ INCREMENTAL │ │
│ │ barrier. │ │ Mark value. │ │
│ └──────────┘ │ Add to worklist. │ │
│ └──────────────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Generational heap | Two generations: young and old |
| Young generation | Collected by scavenger (Cheney copying) |
| Old generation | Collected by mark-sweep (concurrent) |
| Bump-pointer allocation | Allocation is a pointer increment |
| Semi-spaces | Young generation split into from and to |
| Promotion | Objects surviving scavenges moved to old space |
| Remembered set | Old objects that reference young objects |
| Write barrier | Checks stores for cross-generation references |
| Concurrent marking | Marker runs while mutator continues |
| Tri-color abstraction | White, grey, black states during marking |
| Safepoint | Thread synchronization point for GC |
| Floating garbage | Unreachable old objects referenced by young |
| Compaction | Sliding compactor during idle intervals |
Key takeaways:
- Dart’s collector is generational because most objects die young. The weak generational hypothesis holds for object-oriented workloads, and Flutter’s widget-heavy rendering confirms it. Separating young and old objects lets the collector focus effort where the garbage is.
- The scavenger is a copying collector. It copies live objects to a new semi-space and abandons the old one. The cost is proportional to survivors, not allocations. This makes it ideal for the rapid allocation and destruction pattern of UI frameworks.
- Bump-pointer allocation is nearly free. Allocating an object in the young generation is a pointer increment in a thread-local buffer. There is no free-list search and no locking.
- Promotion moves survivors to the old generation. Objects that survive a threshold number of scavenges are copied to old space, where they are managed by mark-sweep.
- Mark-sweep uses concurrent marking to minimize pauses. The marker traverses the object graph while the mutator continues. Write barriers ensure that pointers created during marking are not missed.
- The remembered set tracks old-to-young references. Without it, the scavenger would miss young objects reachable only from old objects. The write barrier adds old objects to the set when they store a pointer to a young object.
- New-space objects are treated as roots during old-generation marking. This avoids synchronizing the concurrent marker with the scavenger, at the cost of some floating garbage.
- The write barrier has two modes. The generational barrier tracks old-to-young stores. The incremental barrier tracks old-to-unmarked-old stores during concurrent marking. Both are combined in a single header check.
- Scheduling lets the collector run during idle periods. The Dart VM coordinates with the Flutter engine to run collections and compaction when the application is not rendering a frame.
Remember: Dart’s garbage collector is a two-generation system. The young generation, collected by the Cheney scavenger, handles the constant stream of short-lived objects that applications create. The old generation, collected by concurrent mark-sweep, handles the objects that survive and accumulate. The scavenger copies survivors and abandons the rest, making reclamation proportional to the number of live objects rather than the number allocated. The mark-sweep collector uses concurrent marking and write barriers to keep pauses short, and treats new-space objects as roots to avoid synchronization. The two collectors interact through promotion and the remembered set, and the growth policy must interleave their collections to prevent floating garbage from accumulating. Understanding this design is what allows a developer to reason about allocation cost, pause behavior, and why Flutter can create thousands of widgets per frame without fear of the garbage collector.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!