Middle · Trace · Retaining paths, cycles and dominators

Trace the portable program: what is guaranteed, and what does it not measure?

Think through your answer before opening the explanation.

Memory & GC

var roots = new Dictionary<string, Node>();
var left = new Node("left"); var right = new Node("right");
left.Next = right; right.Next = left;
roots["cache"] = left; roots["request"] = right;
roots.Remove("request");
Console.WriteLine($"owners:{roots.Count}");
Console.WriteLine($"cycle:{ReferenceEquals(roots["cache"].Next!.Next, left)}");
roots.Clear();
Console.WriteLine($"registered-owners:{roots.Count}");
GC.KeepAlive(left); GC.KeepAlive(right);
sealed class Node(string name) { public string Name = name; public Node? Next; }
Reveal answer and reasoning

The portable program constructs left and right, creates a cycle, and registers two owners in a dictionary. Removing request leaves cache as the one registered owner, so owners:1 is printed. Following cache to left, then Next to right, then Next back to left returns the same instance, producing cycle:True. Clearing the dictionary changes its membership count to zero.

Both local variables are deliberately kept alive through explicit KeepAlive calls. Consequently registered-owners:0 does not establish that the cycle is unreachable in this execution. The program demonstrates dictionary ownership and identity, not a collection event. In the interactive scenario, the root set is explicitly defined by the controls, so a disconnected cycle can be reclaimed by that model. Keep those two forms of evidence separate when explaining the lesson.

Start with a representative workload and a precise time window. Record the live type distribution after comparable lifecycle boundaries, then inspect roots of selected representative instances using a matching diagnostic tool. In dotnet-dump analysis, dumpheap can locate candidates and gcroot can explain why a particular object is still reachable. An address is valid in that dump's context; do not reuse it as a stable object identifier across later collections or different processes.

A root path is a clue to ownership, not a complete business verdict. A static singleton might intentionally retain configuration. A request task might legitimately retain a buffer until a slow client completes. Compare the observed lifetime with the intended contract. Capture enough surrounding context to distinguish a real cache policy failure from a snapshot taken during ordinary peak work.

What a strong answer covers

  • Tracing collection can reclaim disconnected cycles.
  • A dominator lies on every path from the synthetic root.
  • Shallow size is not the retained size of an ownership subtree.

Common pitfall

Calling every cycle a leak, or counting a shared backing array in full for each owner.

Follow-up questions

  1. How does a second independent root change retained-size attribution?
  2. What strong local does a successful TryGetTarget introduce?

Self-review

Choose what to revisit. These marks are your own assessment and are separate from lesson completion.

Primary references

Open lesson and code ↗

← All interview questions