I've been studying .NET GC internals and wanted to go beyond reading docs — so I built a small experiment suite to make the behavior actually visible.
This post walks through what I learned and what each experiment demonstrates. The full repo is at the end.
Why experiments?
Documentation tells you what the GC does. Running code tells you when and why. The difference matters when you're trying to reason about allocation pressure in real systems.
The project is called GCExperiment — four isolated experiments, each targeting one GC mechanic, all instrumented with real snapshots.
Built on .NET 10 / C# 14, x64 only. Header sizes and alignment differ on x86, so 64-bit is required for realistic LOH behavior.
Experiment 1 — LOH Placement
The most surprising thing I learned: it's not a simple 85,000-byte threshold.
The GC measures the full object cost — header, payload, and alignment. Here's the actual math for byte[84_999]:
24 bytes (array header)
+ 84,999 (payload)
= 85,023 → aligned to 85,024 → LOH threshold hit
So byte[84_999] goes to the Large Object Heap — not because the payload exceeds 85K, but because the aligned full object does.
What the experiment shows:
- Where arrays of different sizes actually land (SOH vs LOH)
- How to measure full object size with
GCInfo.EstimateSizeByFactory - How to force LOH compaction when needed:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Forced, blocking: true);
Experiment 2 — Generation Promotion
Objects don't live forever in Gen0. Survivors get promoted — and this experiment makes that visible in real time.
GC.GetGeneration(obj) |
Meaning |
|---|---|
0 |
Recently allocated, ephemeral |
1 |
Survived one Gen0 collection |
2 |
Long-lived, or on the LOH (LOH objects start at Gen2 immediately) |
One thing I didn't expect: GC.KeepAlive genuinely changes outcomes when you're observing promotion. Without it, the JIT can optimize away your reference and the object gets collected before you can inspect its generation.
Experiment 3 — Allocation Pressure
This one compares two patterns side by side:
- Short-lived small objects — allocate, discard, repeat → hammers Gen0
- Long-lived large objects → builds Gen2/LOH pressure
The output prints live collection counts as the experiment runs. Changing sample sizes and watching how Gen0 vs Gen2 counts diverge is genuinely useful for building intuition.
Experiment 4 — LOH Fragmentation
This one was the most eye-opening.
If you alternate large allocations — allocate A, allocate B, free A, try to allocate C — the hole left by A may not fit C. The LOH is not compacted by default, so those holes accumulate.
[ A ][ B ][ hole ][ B ][ hole ]
↑
C doesn't fit here
This is why LOH compaction and buffer pooling (ArrayPool<T>) exist. The experiment makes the fragmentation visible through allocation failures and GC snapshot comparisons.
Diagnostics & Helpers
The repo includes two instrumentation helpers:
GCMonitor (in GCMastery.Diagnostics)
GCMonitor.PrintSnapshot("after allocation");
GCMonitor.PrintObjectGeneration(myObj, nameof(myObj));
GCMonitor.Reset("clean baseline");
GCInfo (in GCExperiment)
var state = GCInfo.GetCurrentState(forceFullCollectionFirst: true);
var gen = GCInfo.GetGeneration(myObj);
var size = GCInfo.EstimateSizeByFactory(() => new byte[84_999], sampleCount: 1000);
Running Everything
GenerationExperiment.Run();
LOHExperiment.Run();
FragmentationExperiment.Run();
PressureExperiment.Run();
One tip before measuring anything — always start clean:
GC.Collect(2, GCCollectionMode.Forced, blocking: true);
GC.WaitForPendingFinalizers();
GC.Collect(2, GCCollectionMode.Forced, blocking: true);
Otherwise leftover state from previous runs pollutes your results.
What I'm Still Figuring Out
- Server GC vs Workstation GC behavior differences
- How
GCSettings.LatencyModeaffects promotion timing - Better ways to visualize fragmentation over time (currently console-only)
If you've worked with any of this and spot something off — or have ideas for experiments worth adding — I'd love the feedback in the comments.
Repo
👉 https://github.com/Shuvo091/GCExperiment
.NET 10 · C# 14 · x64 required
Top comments (0)