Memory & Garbage Collection · Lesson 18

What GC.Collect cannot fix

A requested collection does not make a strongly reachable object disappear.

Explore the interactive Memory Lab

Change owners, step through generations, pinning and finalization, or compare memory under load. The model explains concepts; the SDK examples measure real runtime observations.

Open the Memory Lab ↗

The memory contract

GC.Collect requests collection work according to the runtime's supported behavior; it does not override reachability or repair an ownership contract. A cache, event publisher, closure, or active local can continue to keep an object alive after a collection request. Induced collections can also disrupt a collector's own scheduling heuristics and add pauses. This lesson makes one bounded educational call to expose the liveness rule, not to recommend calling Collect routinely after every operation or to manufacture deterministic finalizer timing.

Trace the actual program

The owner strongly references a Payload, and a weak reference observes that same object. The program requests a collection, then tries to obtain the target and prints True. KeepAlive extends the strong owner's required liveness through the observation, so this result does not depend on guessing when a source-level local becomes dead. The sample contains no finalizer, no resource-release print, and no resident-memory measurement. It proves only that an explicitly live target remains available; it does not prove how much collector work occurred or that calling Collect improved anything.

How the mechanism works

For CoreCLR, collection policy normally uses allocation pressure and survival information to choose work. Manually requesting collections changes that policy and can promote survivors or impose unnecessary full-heap work depending on the request and implementation. Other runtimes interpret their supported GC APIs under different collector designs. Even when unreachable managed objects are reclaimed, the runtime may retain heap regions for future allocations instead of immediately reducing operating-system memory. Collection, compaction, heap reservation, and external resource disposal therefore remain separate lifecycle and accounting questions.

Boundaries and misconceptions

A loop calling Collect until a process graph looks smaller can hide a missing retaining-edge fix and produce misleading performance conclusions. It also cannot reliably make finalizers run at a precise point or close a borrowed resource whose owner never disposed it. There are specialized scenarios for induced collections, but they require an explicit workload, target, latency policy, and evidence. This course does not turn one harmless demonstration into universal tuning advice or assume that every browser and Unity profile shares CoreCLR's scheduling semantics.

Practice: change one ownership path

First remove only GC.Collect and predict that the weak lookup still prints true while owner is kept alive. Hint: strong reachability, not the collection request, guarantees the target remains. Then create another strong owner in a list and explain why dropping the original variable would still leave that path. Draw every retaining edge before proposing another collection call. The exercise should make the ownership explanation stronger, not try to force a false weak-reference result by repeatedly pressuring the runtime.

Practice: test a second boundary

Next replace the strong-owner demonstration with a helper that returns an optional weak reference after ending its own local lifetime. Record possible observations without asserting exactly when the target disappears. Hint: eligibility is not a collection schedule, and temporary or diagnostic references can matter. Design a real retention fix by clearing the documented owning cache or ending an event subscription, then verify those lifecycle transitions directly. Measure the resulting workload on its target rather than adding Collect as a substitute for the missing ownership boundary.

Review the evidence

The unchanged True output is deliberately narrow and reproducible because KeepAlive states the liveness requirement. A correct explanation does not describe it as a broken collector or proof that weak references keep targets alive. When considering an induced collection, first ask what symptom it is intended to solve and whether a retained root, unmanaged resource, or measurement mismatch is the actual cause. The default response to memory growth should be investigation, not a blanket instruction to force collection after every task.

  • The one induced collection is an educational experiment, not a tuning recommendation.
  • The strong owner remains live, so the weak lookup prints True.
  • Collect does not remove roots, dispose resources, or guarantee process-memory shrinkage.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

var owner = new Payload("kept");
var weak = new WeakReference<Payload>(owner);
GC.Collect();
Console.WriteLine(weak.TryGetTarget(out _));
GC.KeepAlive(owner);
record Payload(string Name);

Expected output

True
Try this
The one induced collection is an educational experiment, not a tuning recommendation.

Further reading