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
- How does a second independent root change retained-size attribution?
- 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.