100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Garbage Collection in D

How D's built-in conservative garbage collector manages class instances, dynamic arrays, and closures, and how to control or work around it.

Memory & TypesIntermediate9 min readJul 10, 2026
Analogies

Garbage Collection in D

D ships with a built-in garbage collector that automatically manages memory for class instances allocated with new, dynamic array growth, associative arrays, and closures that capture variables by reference. The default D GC is a conservative, mark-and-sweep, stop-the-world collector: conservative means it scans memory (including the stack and CPU registers) for bit patterns that look like they could be pointers into the GC heap, without requiring precise type information at every allocation, and stop-the-world means that during a collection cycle all other threads managed by the runtime are paused while the collector marks reachable objects and sweeps the rest. This design trades some memory precision and pause-time predictability for simplicity and safety: you rarely need to think about freeing memory explicitly for ordinary D code, which is why idiomatic D leans on the GC by default and only opts out selectively where determinism or latency matters.

🏏

Cricket analogy: The GC's stop-the-world pause is like a rain delay where the entire match halts — both teams and the umpires — while ground staff (the collector) clear covers and inspect the pitch, before play (all threads) resumes together.

Controlling the Collector

The core.memory module exposes the GC struct with static functions for direct control: GC.collect() forces a full collection cycle immediately rather than waiting for the runtime to decide one is needed, GC.disable() and GC.enable() toggle whether automatic collections are allowed to run at all (allocations still succeed while disabled, they just accumulate without being collected), and GC.stats() returns a snapshot with usedSize and freeSize letting you inspect heap usage for diagnostics or tests. When your program holds memory that the GC did not allocate — for example a block obtained from malloc that you want scanned for embedded D references, or memory-mapped external memory — you can register it with GC.addRange(ptr, size) so the collector includes it when scanning for live pointers, and unregister it later with GC.removeRange(ptr) once it's no longer relevant. These controls are mostly used for tuning latency-sensitive code paths (like temporarily disabling collection during a real-time audio callback) or for writing precise unit tests that assert on allocation counts.

🏏

Cricket analogy: GC.disable() before a critical over is like a captain requesting the ground staff hold off on any pitch inspection until the over finishes, then GC.enable() lets normal ground maintenance resume afterward.

@nogc and Avoiding Allocation Pressure

The @nogc attribute can be attached to a function signature, void process() @nogc { ... }, and the compiler then statically enforces that the function's body performs no operation that could trigger a GC allocation — no new for classes, no array literal that allocates, no closure capture that needs heap storage — flagging a compile error if it does. This is valuable in real-time or latency-sensitive code (audio processing, game loops, signal handlers) where an unpredictable GC pause is unacceptable; @nogc functions typically rely on stack-allocated structs, static arrays, or manually managed memory instead. Beyond @nogc, general techniques for reducing GC pressure include using std.array's reserve to pre-size a dynamic array before a loop of appends (avoiding repeated reallocation), preferring structs over classes for short-lived, non-polymorphic values, and reusing buffers across iterations instead of allocating a fresh array on every call.

🏏

Cricket analogy: @nogc on a hot function is like a fielding coach banning any player from calling for a drinks break mid-over — the routine must complete using only what's already on the field, with zero unplanned stoppages.

Common GC Pitfalls

Because the D GC scans conservatively, it can occasionally treat an ordinary integer value on the stack that happens to look like a valid heap pointer as a genuine reference, keeping the object it 'points to' alive longer than necessary — this is called a false pointer, and while it cannot cause a use-after-free, it can cause memory that should have been reclaimed to leak for the remainder of the program (or until the false pointer's stack slot is overwritten). Stop-the-world pauses scale with the number of live objects and heap size, so a program with a very large live object graph can experience noticeably longer pauses at collection time, which matters for interactive applications and real-time systems even though it is rarely a problem for typical batch or server workloads. Finally, because class destructors run at GC-determined times, code that assumes a resource wrapped in a class (a file handle, a socket) will be released promptly when the last reference goes out of scope is simply wrong in D — that assumption holds for structs with deterministic destructors, not for GC-managed classes, and mixing up the two is one of the most common sources of resource-leak bugs for programmers coming from RAII-heavy languages.

🏏

Cricket analogy: A false pointer keeping an object alive is like a scorer accidentally keeping a retired player's name on the active XI sheet because an old team-sheet scrap that merely looks similar to the current lineup was left lying nearby.

d
import std.stdio;
import core.memory : GC;

@nogc void sumStatic(ref const int[4] values, out int result) nothrow
{
    result = 0;
    foreach (v; values)
        result += v; // no heap allocation anywhere in this function
}

void main()
{
    int[4] data = [1, 2, 3, 4];
    int total;
    sumStatic(data, total);
    writeln("total = ", total);

    // Inspect current heap usage
    auto stats = GC.stats();
    writeln("used: ", stats.usedSize, " free: ", stats.freeSize);

    // Temporarily disable automatic collection around a latency-sensitive block
    GC.disable();
    scope(exit) GC.enable();

    auto buffer = new int[1000]; // still allocates; just won't be auto-collected yet
    writeln("buffer length: ", buffer.length);

    GC.collect(); // force a collection explicitly when convenient
}

D's GC is conservative: it scans the stack, registers, and the GC heap itself for values that look like pointers, without requiring precise type metadata at every allocation site. This trades some precision for implementation simplicity and safety.

Calling GC.disable() without a matching plan to periodically call GC.collect() (or re-enable it) lets allocations accumulate unbounded, since nothing is ever reclaimed while disabled — this can exhaust available memory in a long-running process.

  • D's built-in GC automatically manages class instances, dynamic array growth, associative arrays, and reference-capturing closures.
  • The default collector is conservative (scans for pointer-like bit patterns) and stop-the-world (pauses all managed threads during collection).
  • core.memory.GC exposes collect(), disable(), enable(), stats(), addRange(), and removeRange() for manual control.
  • @nogc statically enforces that a function performs no GC allocation, useful for real-time or latency-sensitive code paths.
  • reserve(), reusing buffers, and preferring structs over classes for short-lived values all reduce GC allocation pressure.
  • Conservative scanning can produce false pointers that delay reclamation but never cause use-after-free.
  • Class destructors run at GC-determined, nondeterministic times — never assume prompt resource release the way RAII structs provide.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#DProgrammingStudyNotes#GarbageCollectionInD#Garbage#Collection#Controlling#Collector#StudyNotes#SkillVeris#ExamPrep