How does Dart handle memory management?

Medium PriorityAsked in ~50% of Flutter interviews

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:

GenerationHoldsGC typeSpeed
Young (new space)Freshly allocated objectsScavenger (copy live, drop the rest)Fast, frequent
Old (old space)Long-lived survivorsMark-sweep + compactionSlower, 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

SymptomLikely cause
Memory climbs on every navigationUndisposed controller / subscription on the screen
Memory climbs over hours of useCache without eviction, growing log buffer
Spike + crash on opening a screenLoading too-large images without resizing
Jank during scrollGC 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.