How does Dart handle memory management?
4 min read
Dart Runtime
Dart auto-manages memory with a generational garbage collector — you never free() anything. Your job is to release references (cancel subscriptions, dispose controllers, clear captured closures); the GC does the rest.
Dart's GC is generational:
| Generation | Holds | GC type | Speed |
|---|---|---|---|
| Young (new space) | Freshly allocated objects | Scavenger (copy live, drop the rest) | Fast, frequent |
| Old (old space) | Long-lived survivors | Mark-sweep + compaction | Slower, rare |
Most objects die young (a Future, a temporary list, the Widget tree on rebuild). The young-space scavenger collects them cheaply. Survivors get promoted to old space.
Each isolate has its own heap. Objects can't be shared across isolates — that's why Isolate.run(...) returns a copy.
Code in action — the rules of engagement
void example() {
var user = User('Alice'); // allocated on heap
print(user.name);
} // user goes out of scope → eligible for GC
// (collected later, not immediately)
// The only thing you control is whether something is still REACHABLE.
// As long as something points to it, the GC must keep it alive.
The 4 classic Flutter leaks
// 1️⃣ Uncancelled StreamSubscription
class _MyState extends State<MyWidget> {
late StreamSubscription _sub;
@override
void initState() {
super.initState();
_sub = stream.listen(_onData);
}
@override
void dispose() {
_sub.cancel(); // ✅ release the listener
super.dispose();
}
}
// 2️⃣ Undisposed controllers
final _scroll = ScrollController();
final _text = TextEditingController();
final _anim = AnimationController(vsync: this, duration: ...);
@override
void dispose() {
_scroll.dispose();
_text.dispose();
_anim.dispose();
super.dispose();
}
// 3️⃣ Long-lived singletons holding callbacks to short-lived widgets
SomeService.instance.onUpdate = () => setState(() {}); // captures `this`
// ✅ Null it out in dispose() or use a weak callback pattern
// 4️⃣ Caches that grow forever
final cache = <String, Image>{}; // no eviction policy → memory bloat
// ✅ Use a bounded cache (lru, or images already cached by Flutter's ImageCache)
Isolates and memory
// Each isolate = isolated heap. Data crosses by COPY.
final result = await Isolate.run(() => heavyJsonParse(bigPayload));
// ↑ runs on a separate heap, can't leak state into your isolate
For one-off CPU work, compute() / Isolate.run() is the easiest path. For long-lived workers, use Isolate.spawn with a SendPort.
When to worry about memory
| Symptom | Likely cause |
|---|---|
| Memory climbs on every navigation | Undisposed controller / subscription on the screen |
| Memory climbs over hours of use | Cache without eviction, growing log buffer |
| Spike + crash on opening a screen | Loading too-large images without resizing |
| Jank during scroll | GC pressure from per-frame allocations |
Common mistakes to avoid
// ❌ Forgetting dispose() on controllers
final _ctrl = TextEditingController(); // leaks until widget tree is GC'd
// ❌ Listening to a Stream without keeping the subscription
stream.listen(handle); // can't cancel — leaks forever
// ❌ Loading full-resolution images into memory
Image.network('https://.../4k.png') // 8000×6000 image in RAM
// ✅ Use cacheWidth/cacheHeight or ResizeImage
// ❌ Holding BuildContext in long-lived objects
class Service { BuildContext? context; } // ties Service lifetime to a screen
// ✅ Pass context only at call time
// ❌ Trying to force GC for "performance"
// You can't — and you usually shouldn't want to. Fix references, not the collector.
Interview follow-ups
-
Can you force garbage collection in Dart? Not from production code. DevTools has a "Force GC" button for debugging, but the runtime decides when to collect. Your lever is reachability — drop references and the GC handles the rest.
-
Why does Flutter ship a
dispose()lifecycle method? To give widgets a hook to release non-GC resources (subscriptions, controllers, native handles) and break reference cycles to long-lived objects. The GC will eventually collect the State, but only after it stops being reachable. -
What is a memory leak in a GC'd language like Dart? A logical leak: the object is unreachable from the user's perspective but still referenced by something else (a singleton, a static field, a registered callback). GC can't help — the reference graph keeps it alive.
-
How is Dart's GC different from JS or Java's? Dart is generational with a fast scavenger for young objects, very similar to V8. Compared to JVM, Dart's heap is per-isolate, so multi-threaded code doesn't share heap state — no shared-memory concurrency, no read/write barriers across threads.
How helpful was this content?
Please sign in to rate this article.