How Dart’s Garbage Collector Works (And When It Fails You!)
Automatic Memory Management: The Dream and the Reality

Automatic Memory Management: The Dream and the Reality
Free link for readers
Dart, the language powering Flutter, employs automatic memory management through a garbage collector (GC). This means that, unlike languages like C or C++, Dart developers don’t have to manually allocate and deallocate memory. The Dart runtime takes care of it for you, automatically reclaiming memory occupied by objects that are no longer in use.
This is a huge advantage, simplifying development and preventing a whole class of memory-related bugs. But, like any complex system, Dart’s GC isn’t perfect. There are situations where it can fail you, leading to memory leaks and performance issues.
This article delves into the inner workings of Dart’s garbage collector, explores its strengths, and shines a spotlight on its Achilles’ heel: when things go wrong.
Dart’s Garbage Collector: A Generational Approach
Dart’s GC is a generational garbage collector. This means it divides the heap (the memory area where objects are stored) into different generations, primarily:
- Young Generation (New Space): This is where newly created objects are allocated. The assumption is that most objects are short-lived, so this space is designed for very fast collection.
- Old Generation (Old Space): Objects that survive a few collections in the young generation are promoted to the old generation. This space contains longer-lived objects and is collected less frequently.
This generational approach is based on the observation that most objects have short lifespans. By focusing on collecting the young generation more often, the GC can efficiently reclaim most of the garbage with minimal overhead.
The Mark-and-Sweep Algorithm (with a Twist)
Dart’s GC primarily uses a mark-and-sweep algorithm, but with some optimizations:
- Mark Phase: The GC starts from a set of root objects (objects directly accessible by the program, like global variables and objects on the stack). It then traverses the object graph, marking all the objects that are reachable from these roots.
- Sweep Phase: The GC scans the heap and collects all the objects that were not marked in the mark phase. The memory occupied by these unreachable objects is then reclaimed.
Key Optimizations in Dart’s GC:
- Generational Collection: As explained earlier, this significantly improves efficiency.
- Concurrent Garbage Collection: Dart’s GC performs many of its operations concurrently with the program’s execution, reducing “stop-the-world” pauses. These pauses are interruptions where the application freezes while the GC is running. Dart minimizes these pauses to provide a smoother user experience, which is crucial for Flutter’s interactive applications.
When the Dream Turns into a Nightmare: Memory Leaks
Despite the sophistication of Dart’s GC, memory leaks can still occur. A memory leak happens when an object is no longer needed by the application but is still being referenced, preventing the GC from reclaiming its memory.
Here are some common scenarios in Dart and Flutter that can lead to memory leaks:
- Unclosed Streams and Subscriptions:
- Streams are a fundamental part of Dart, used for handling asynchronous data. If you create a stream subscription and forget to cancel it, the subscription will continue to hold a reference to the stream and any listeners, even if they are no longer needed.
- Code Example:
import 'dart:async';
class MyWidget {
StreamSubscription? _subscription;
void startListening() {
final stream = Stream.periodic(const Duration(seconds: 1), (i) => i);
_subscription = stream.listen((data) {
print('Received: $data');
});
// Oops! We forgot to cancel the subscription in dispose()!
}
void dispose() {
// Missing: _subscription?.cancel();
}
}
- Why it leaks: The
_subscription
holds a reference to the stream, preventing the stream and its resources from being garbage collected. - The fix: Always cancel stream subscriptions in the
dispose()
method of your stateful widgets or classes.
2. Listeners and Callbacks:
- Many Dart and Flutter APIs involve listeners or callbacks. If you register a listener to an object (e.g., an
AnimationController
, aChangeNotifier
, or a custom event emitter) and forget to remove it, the listener will hold a reference to your object, preventing it from being garbage collected. - Code Example:
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
final _notifier = ChangeNotifier();
@override
void initState() {
super.initState();
_notifier.addListener(_myListener); // Add listener
}
void _myListener() {
print('Listener called!');
}
@override
void dispose() {
_notifier.removeListener(_myListener); // Remove listener
_notifier.dispose();
super.dispose();
}
}
- Why it leaks: If you forget to call
_notifier.removeListener(_myListener)
, the_notifier
will hold a reference to_MyWidgetState
, preventing the state from being garbage collected. - The fix: Always remove listeners in the
dispose()
method. Also, dispose of anyChangeNotifier
or other disposable objects.
3. Closures and Capturing Variables:
- Dart closures can “capture” variables from their surrounding scope. If a closure is held onto for a long time, it can prevent the captured variables (and the objects they reference) from being garbage collected.
- Code Example:
class MyClass {
final int id;
MyClass(this.id);
void doSomething() {
print('Doing something with id: $id');
}
}
void main() {
MyClass myObject = MyClass(123);
Function? myClosure;
void createClosure() {
myClosure = () {
myObject.doSomething(); // Capture myObject
};
}
createClosure();
// myClosure is held onto, preventing myObject from being GC'd
// even if it's no longer used elsewhere.
}
- Why it leaks: The
myClosure
function capturesmyObject
. As long asmyClosure
is held in memory,myObject
cannot be garbage collected, even ifmyObject
is no longer used elsewhere in the program. - The fix: Avoid creating long-lived closures that capture large objects. If you must, consider using weak references (see below) or designing your code to minimize the lifetime of captured variables.
4. Static Variables:
- Static variables live for the entire duration of the program’s execution. If a static variable holds a reference to an object, that object will never be garbage collected.
- Code Example:
class MyClass {
final List<int> data = [];
}
class Global {
static MyClass myObject = MyClass(); // Static variable holding an object
}
void main() {
// Global.myObject is never garbage collected.
}
- Why it leaks:
Global.myObject
is a static variable, so it persists for the entire lifetime of the application. TheMyClass
instance it holds will never be garbage collected. - The fix: Use static variables sparingly, and avoid storing long-lived objects in them. If you must use a static variable, consider setting it to
null
when the object it holds is no longer needed.
Weak References: A Ray of Hope
Dart provides WeakReference
objects, which allow you to refer to an object without preventing it from being garbage collected.
- How they work: A
WeakReference
holds a reference to an object, but the garbage collector is allowed to collect the object as if theWeakReference
didn't exist. - How to use them:
import 'dart:core';
void main() {
final myObject = MyObject();
final weakRef = WeakReference(myObject);
// ... later ...
if (weakRef.target != null) {
// Use the object
weakRef.target!.doSomething();
} else {
// The object has been garbage collected
print('MyObject has been garbage collected!');
}
}
class MyObject {
void doSomething() {
print('Doing something!');
}
}
- Important: You must always check if
weakRef.target
is null before using it, because the object it references might have been garbage collected. - Use cases: Weak references are useful for implementing caches, observers, and other patterns where you need to refer to an object without preventing its garbage collection.
Finalizers: Running Code Before Collection
Dart also provides Finalizer class, which allows you to attach a callback to an object that will be executed when the object is garbage collected.
- How it works: You create a
Finalizer
and attach it to an object. When the object is garbage collected, theFinalizer
's callback is invoked. - How to use them:
import 'dart:io';
import 'dart:isolate';
final finalizer = Finalizer<int>((value) {
print('Finalizer called with value: $value');
// IMPORTANT: Finalizers run in an arbitrary isolate.
// DO NOT interact with the main isolate's state here.
// Use a SendPort to send data back to the main isolate if needed.
File('my_resource_$value.txt').deleteSync(); // Example: Clean up a resource
});
class MyResource {
MyResource(this.id) {
finalizer.attach(this, id); // Attach the finalizer to this object
File('my_resource_$id.txt').createSync(); // Create a resource
}
final int id;
void dispose() {
finalizer.detach(this); // Detach finalizer if object is manually disposed.
File('my_resource_$id.txt').deleteSync();
}
}
void main() {
final resource1 = MyResource(1);
final resource2 = MyResource(2);
// resource1 will be garbage collected when it's no longer used.
// The finalizer will be called, and "my_resource_1.txt" will be deleted.
resource1.dispose(); // Manually dispose resource1 and detach finalizer
}
- Use cases: Finalizers are useful for cleaning up external resources, such as file handles, sockets, or native memory, that are associated with an object.
Important Considerations:
- Finalizers run in a separate isolate, not the main isolate. This means you cannot directly access or modify the main isolate’s state from within a finalizer. If you need to communicate with the main isolate, use a
SendPort
. - Finalizers are not guaranteed to run immediately when an object is garbage collected. The execution of finalizers is managed by the Dart runtime and can be delayed.
- Avoid performing complex or time-consuming operations in finalizers, as this can impact performance.
- Finalizers should not be used for managing Dart memory.
The Takeaway: Be Mindful, Not Fearful
Dart’s garbage collector is a powerful tool that simplifies memory management and prevents many common bugs. However, it’s not a silver bullet. Memory leaks can still occur if you’re not careful.
“The best garbage collector is the one that doesn’t need to run.” — Unknown
While you don’t need to become a memory management expert, it’s essential to be aware of the potential pitfalls. By following best practices, such as:
- Always closing streams and canceling subscriptions.
- Removing listeners when they are no longer needed.
- Being mindful of closures and captured variables.
- Avoiding long-lived objects in static variables.
- Using
WeakReference
andFinalizer
when appropriate - Profiling your app’s memory usage with Dart DevTools.
and you can write efficient, performant, and leak-free Dart code.