.NET Memory & Garbage Collection

Sagar HSSagar HS
6 min read

(covers .NET 8 → 9 optimisations, realistic stack-vs-heap guidance, boxing costs, LOH tuning, GC-mode selection, and diagnostics)

TL;DR - Think “Stack first, Heap when you must.”

The thread-stack is a lightning-fast workbench cleared automatically when a method returns, the managed heap is a huge warehouse that needs the GC’s fork-lifts to tidy up. Put quick-use tools (tiny structs, scratch buffers) on the bench, store long-term gear (shared objects, large arrays) in the warehouse. Every GC run is a warehouse-wide sweep that can stall workers, so minimise how often you trigger it.

Why the Stack RocksPragmatic, real-world Actions
Zero GC cost – pushing/popping is just pointer math.• Use small value-type structs for immutable payloads.
• Slice existing data with Span<T> / ReadOnlySpan<T> instead of allocating new arrays.
CPU-cache friendly – data lives beside your call-frame.• Inline small structs inside parent objects when locality matters.
Predictable latency – never pauses for GC.• Allocate tight scratch space with stackalloc on the hottest paths.
No fragmentation worries – whole frame vanishes on return.• Keep each per-call buffer modest (< 16 KB) to avoid stack overflows.

Arrays are fine - just pool or reuse large ones so they don’t keep bloating the Large-Object Heap (LOH).


1. Memory 101 - Kitchen Counter vs. Warehouse Analogy

  • Thread Stack = Kitchen Counter: small, fast, cleared when the cook (method) finishes.

  • Managed Heap = Warehouse: huge, shared across cooks, needs a night-shift cleaning crew (GC) to keep it tidy.

The CLR bumps a pointer for each new heap allocation cheap until the segment fills, then stops the world for a cleanup cycle.

Span<int> scratch = stackalloc int[4]; // goes on the “counter”
scratch[0] = 42;

2. Value Types, Reference Types & Boxing : Why That One int Hurts

  • Value types (structs) live on the counter (stack) or inline in arrays.

  • Reference types (classes, arrays) live in the warehouse (heap) and are reached via pointers on the stack.

object boxed = 42; sneakily wraps the int in a heap object, creating Gen 0 garbage every loop iteration. Swap to generics (List<int>), spans, or in parameters to avoid accidental boxing.


3. Generational Heap - Three Recycling Bins and a Bulk-Trash Corner

RegionTypical Size*CompactionUse-case
Gen 0256 KB–2 MB“Paper scraps” - most objects die here
Gen 1≈ 4×Gen 0Buffer for survivors
Gen 2Up to proc limitLong-living singletons, caches
LOH≥ 85 KBBulk trash - big arrays/bitmaps
POH (.NET 5+)pinnedBuffers shared with native code

\segment sizes expand/contract with allocation rate.* Objects that endure one pickup are simply promoted - no data copy needed, so 90 % of collections touch only Gen 0.


4. What Exactly Happens During a GC?

  1. Mark: trace from roots (stacks, statics, CPU registers) and paint live objects.

  2. Plan/Relocate: compute new addresses for survivors.

  3. Compact: slide Gen 0/1/2 objects together to close gaps.

  4. Fix-up: patch every reference so pointers stay valid.

A tiny write-barrier on every ref assignment updates a card table, letting the GC skip untouched memory pages and finish faster.


5. Choosing a GC Mode (and Tuning It)

ModeBest-fit ScenarioHeaps per ProcessPause Pattern
WorkstationDesktop/UI, CLI tools, memory-tight containers1Ultra-short, single-threaded
ServerHigh-throughput APIs & micro-services1 per logical CPUParallel, fewer but longer
BackgroundOverlay for both - adds concurrent Gen 2 collectioninheritsMicro-stalls only
<!-- Throughput-optimised worker service -->
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

Key knobs you might touch:

Config FlagWhy Touch It
GarbageCollectionAdaptationModeControls DATAS auto-shrink (0 = off).
GCHeapHardLimitPercentCap heap in small containers.
DOTNET_GCHighMemPercentPull full-GC forward/back based on RAM pressure.
GCLatencyMode (API)Switch to LowLatency during real-time bursts.

Workstation maximises low pause/low footprint, Server maximises throughput, Background keeps Gen 2 sweeping concurrent so UI never freezes.

6 .NET 8 & .NET 9 - What Changed Under the Hood?

ReleaseGC / Runtime UpgradePractical Win
.NET 8Faster concurrent marking; smarter full-GC trigger; leaner finalizer queue; Native-AOT readyP99 latency drops ~5 % on typical APIs
.NET 9DATAS ON by default (Server GC) so heaps shrink when traffic calms, cross-platform vxsort speeds Gen 2 compactio, JIT can stack-allocate provably non-escaping boxesUp to 20 % memory drop & 15 % lower tail-latency in real load tests

Disable DATAS only if you must keep fixed-size heaps:

<PropertyGroup>
  <GarbageCollectionAdaptationMode>0</GarbageCollectionAdaptationMode>
</PropertyGroup>

7. LOH & Realistic Array Strategy

Large arrays (≥ 85 KB) skip compaction, so holes build up like Swiss cheese.
Pool big buffers with ArrayPool<T> or keep a single reusable field. Don’t fear arrays, just avoid spraying fresh 100-KB blocks each request.

byte[] buf = ArrayPool<byte>.Shared.Rent(128 * 1024);
try { /* work */ }
finally { ArrayPool<byte>.Shared.Return(buf); }

8. Diagnostics in Three Commands

ToolQuick StartWhat You See
dotnet-countersdotnet-counters monitor -p 1234Live Gen counts, heap size
dotnet-gcdumpdotnet-gcdump collect -p 1234Heap snapshot → open in VS
PerfViewRecord → view “GCStats”Timeline of pauses vs. requests

Generate .gcs traces to compare pause length across GC modes and catch regressions before prod.


9. Reality-Checked Best Practices

  1. Design for short-lived allocations – most objects should die in Gen 0.

  2. Kill boxing – generics & spans keep primitives on the stack.

  3. Pool or reuse big buffers – keeps the LOH tidy.

  4. Use Span<T> / Memory<T> for zero-allocation slicing.

  5. Pick the right GC mode – Server + DATAS for services; Workstation for GUIs.

  6. Profile first, tune second – data beats folklore every time.


Key Takeaways

The GC is your friend when you respect the heap*.*
Lean on the stack for high-frequency, short-lived work, let the heap host genuinely shared or long-lived objects. With .NET 9’s smarter, self-shrinking Server GC and disciplined allocation patterns, you ship apps that stay fast, memory-slim, and pause-free.


Further Reading

0
Subscribe to my newsletter

Read articles from Sagar HS directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sagar HS
Sagar HS

Software engineer with 4 + years delivering high-performance .NET APIs, polished React front-ends, and hands-off CI/CD pipelines. Hackathon quests include AgroCropProtocol, a crop-insurance DApp recognised with a World coin pool prize and ZK Memory Organ, a zk-SNARK privacy prototype highlighted by Torus at ETH-Oxford. Recent experiments like Fracture Log keep me exploring AI observability.