Dart uses a generational garbage collection approach, which is based on the idea that most objects die young. So, the heap is divided into two main areas — the new space (for short-lived objects) and the old space (for long-lived objects).
When objects are first allocated, they’re placed in the new space, which is further divided into two semi-spaces, often called from-space and to-space. The garbage collector runs a scavenger, which is a fast copying collector for this area. It copies live objects from from-space to to-space and then swaps them. This makes allocation extremely efficient because Dart just keeps bumping a pointer until it fills up the semi-space.
When an object survives several scavenger collections, it’s promoted to the old space. The old space is managed by a mark-sweep collector, which is more expensive but runs less frequently. The mark phase identifies live objects starting from the roots (like global variables and stack references), and the sweep phase reclaims the memory used by unreachable ones.
A practical example — in a Flutter app I worked on, we noticed jank during animations. Profiling revealed frequent minor GCs due to large numbers of short-lived widget objects being created every frame. To mitigate this, I optimized widget rebuilding by using const constructors and reducing unnecessary stateful widget rebuilds, which decreased allocations in the new space and made scavenger collections less frequent.
One challenge I faced was tuning performance when dealing with streams and async operations — since async callbacks can hold references longer than expected, some objects were getting promoted to old space prematurely, leading to occasional major GC pauses. To handle that, I refactored the async flow to release references sooner and used StreamController.broadcast judiciously to reduce retained listeners.
A limitation of Dart’s GC is that, unlike some systems like Java’s G1 or .NET’s concurrent GC, Dart’s major GC can still cause noticeable pauses in very large heaps, especially in server-side Dart. An alternative in some cases is to use Isolates, since each isolate has its own heap and GC — that can effectively distribute memory load and reduce pause times.
Overall, Dart’s GC is quite efficient for typical app workloads — especially UI-heavy apps — but understanding its generational behavior really helps when optimizing performance-critical paths.
