Garbage collection and jank
A GC pause of 20 ms is a dropped frame. ART's collector history is a fifteen-year attempt to make that number independent of how much memory the app is using.
Garbage collection is the one part of the runtime that can stop your UI thread while it works. Every collector Android has shipped is a different answer to the same question: how much can we do while the app keeps running?
TL;DR#
- Dalvik used mark-sweep with stop-the-world pauses; pause time grew with the live heap.
- ART started with CMS (Concurrent Mark Sweep): marking and sweeping moved off the app thread, but compaction did not.
- Android 8.0 made Concurrent Copying (CC) the default: the heap is compacted while the app runs, and pause time stops scaling with heap size.1
- Android 17 adds generational collection to the Concurrent Mark-Compact collector, so short-lived objects are collected cheaply and often.2
- The cost of a concurrent collector is a read barrier on every reference load — the app pays a little all the time so it does not pay a lot at once.
Mental model#
Think about tidying a workshop.
Stop and tidy is Dalvik: everyone leaves, you sort everything, everyone comes back. Fast and simple, but the workshop is unusable while it happens, and the bigger the workshop the longer the shutdown.
Tidy while people work is CMS: you sweep up in the background. It works until you need to move the benches — rearranging furniture cannot happen while someone is using it.
Move things while people work is CC: you pick up each object, move it, and leave a forwarding note so anyone who was about to use the old location gets sent to the new one. Higher overhead per action, but no shutdown. The forwarding note is the read barrier.
Only tidy the bench you just used is generational collection: most mess is at the workbench you are using right now, so you clean that constantly and the back room rarely.
Where the analogy breaks#
The workshop has one person; an Android app has many threads, and the collector has to coordinate all of them. The read barrier is not a note on a physical object — it is a check inserted into generated code, and its cost depends on what the app is doing. That is why GC tuning is workload-specific and why "the GC is slow" is rarely a complete diagnosis.
The big picture#
Text description of this diagram
Four collector designs shown as timelines of app work. Dalvik mark-sweep shows one long stop-the-world pause covering marking and sweeping. ART CMS shows two short pauses with concurrent marking and sweeping between them, but compaction is not concurrent. ART Concurrent Copying shows very short pauses with copying and reclamation happening concurrently with the app. Generational CMC shows frequent short young-generation pauses and rare full collections. Red bars indicate the app is paused; green indicates the collector is working concurrently.
Switch between the four plans and watch the red bars. The trend is not that pauses disappear — it is that they stop growing with the size of the heap.
Deep dive#
Why GC pauses matter at all#
Android's UI thread has about 16 ms per frame at 60 Hz, and less at higher refresh rates. A garbage collection that stops the world for longer than a frame budget causes a dropped frame, which the user sees as a stutter. If collections are frequent, the stutters are frequent.
The classic Android symptom is a periodic hitch every few seconds during scrolling. It usually correlates with allocation rate: the more objects an app creates per frame, the sooner a collection is triggered.
Dalvik: mark-sweep#
Dalvik's collector was a classic mark-sweep:
- Mark — walk the object graph from roots (thread stacks, statics, JNI references) and mark every reachable object.
- Sweep — walk the heap and return unmarked objects to free lists.
Both phases stop the app. The heap does not move, so objects stay where they were allocated and the free space fragments over time. A large allocation can fail even when the total free memory is plenty, because no single contiguous run is large enough.
ART: CMS#
ART's first production collector was Concurrent Mark Sweep. Marking is split into phases:
- A short initial mark pause to capture roots.
- Concurrent marking while the app runs, using a write barrier (a card table) to catch objects the app modifies during the scan.
- A short remark pause to finish.
- Concurrent sweeping to return memory.
The pauses are short and bounded. But compaction — moving objects to defragment the heap — is not concurrent in CMS: it stops the app. When fragmentation gets bad, CMS pays a long pause anyway.1
ART: Concurrent Copying#
Android 8.0 made Concurrent Copying the default. CC does not mark and sweep; it copies live objects from one region to another while the app runs.
The mechanism that makes this possible is the read barrier. Every reference load in compiled code goes through a check: is this object still where the reference says it is, or has it been moved? If it moved, the barrier forwards the reference to the new copy.
The costs and benefits:
- Benefit: the heap is compacted by construction, and pause time is independent of heap size. The AOSP documentation for Android 8.0 claims a 32% smaller heap, 70% faster allocations, and 85% smaller pause times on one benchmark.3
- Cost: the read barrier slows some workloads, and the collector uses more CPU. Not every app benefits equally.
Generational collection#
Most objects die young. A generational collector exploits that by splitting the heap into a young generation and an old generation.
- A young collection scans only the young generation. It is cheap, so it can run often.
- Surviving objects are promoted to the old generation.
- A full collection scans everything, and happens much less often.
Android 10 enabled generational mode for CC by default: CC runs young or full-heap collections, and chooses a full collection when young-collection throughput falls below the mean full-heap throughput.1
Android 17 adds generational collection to Concurrent Mark-Compact, a different collector from CC.2 The two changes are easy to conflate — both are "generational" and both are about young objects — but they are different collectors with different mechanisms.
What this means for your code#
The collector's design sets the floor on pause time; your allocation pattern sets how often you hit it. The practical rules have not changed in fifteen years:
- Allocate less per frame. Reuse buffers, avoid creating objects in
onDrawand in scroll handlers. - Avoid large temporary collections. A
Listof a thousand objects allocated per frame uses more than the list itself. - Watch the allocation rate, not the heap size. A small heap with a high allocation rate collects constantly.
- Do not assume a pause is free.
System.gc()is a hint, not a command, and a full collection on a large heap is still expensive.
Measuring it#
The tools expose the collector directly. dumpsys gfxinfo reports frame timing, and a periodic
pattern is often a GC signature. ART's own logging can show collection events and pause durations,
which is the only way to tell a GC pause from a slow layout pass.
A useful habit is to separate three numbers:
- Pause time — how long the app was stopped. This is what the user feels.
- GC frequency — how often it happens. Frequent short pauses are worse than rare long ones for perceived smoothness, because each one can drop a frame.
- Total GC time — how much CPU the collector used. This matters for battery and for thermal throttling, especially on a long session.
An app that allocates 5 MB per frame will have all three numbers bad, and no collector change will fix it. That is why the first answer to a GC problem is almost always "allocate less", and the second is "measure the allocation, not the collector".
How it evolved#
| API | Version | Released | What changed |
|---|---|---|---|
| 1 | 1.0 | 2008-09-23 | — |
| 5 | 2.0 Eclair | 2009-10-26 | — |
| 8 | 2.2 Froyo | 2010-05-20 | — |
| 10 | 2.3.3 Gingerbread | 2011-02-09 | — |
| 12 | 3.1 Honeycomb | 2011-05-10 | — |
| 14 | 4.0 Ice Cream Sandwich | 2011-10-18 | — |
| 17 | 4.2 Jelly Bean | 2012-11-13 | — |
| 19 | 4.4 KitKat | 2013-10-31 | — |
| 21 | 5.0 Lollipop | 2014-11-03 | — |
| 26 | 8.0 Oreo | 2017-08-21 | — |
The direction is one-way: fewer, shorter pauses at the cost of more concurrent work. The Android 17 row is the most recent step, and it is the first time in years that a new collector variant has shipped as a default.
Under the hood#
- the collector implementations: mark-sweep, CMS, CC, CMC
- per-collector logic, including concurrent copying
- heap and region accounting
- heap.cccollector selection and triggering
Curiosity corner#
- A GC pause is often blamed on something else. A slow frame after an allocation spike may be the collector, the layout pass, or the renderer; only a trace distinguishes them.
- Compaction is what makes large allocations reliable. Without it, free memory can exist but be unusable.
- Generational collection is an old idea. It was standard on desktop JVMs long before Android adopted it, for the same reason: most objects die young.
Try it#
adb logcat -s art | grep -i "GC freed" | headThe log format differs between ART versions; the fields are illustrative.
adb shell dumpsys gfxinfo com.example.app | head -20Numbers depend on the app and the device; only the field names are stable.
Check yourself#
Check yourself
How is a new app process created from Zygote?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsDebug ART garbage collectionsource.android.comaccessed 2026-10-27↩
- docsBehavior changes: all apps (Android 17)developer.android.comaccessed 2026-10-27↩
- docsART improvementssource.android.comaccessed 2026-10-27↩
- docsAndroid Runtime (ART) and Dalviksource.android.comaccessed 2026-10-27↩
- docsConfigure ARTsource.android.comaccessed 2026-10-27↩
- docsManage your app's memory overviewdeveloper.android.comaccessed 2026-10-27↩