.NETHard
Large Object Heap (LOH) in .NET — when objects go there and how to avoid Gen 2 fragmentation
Any object >= 85 000 bytes is allocated on the LOH instead of Gen 0. LOH is collected with Gen 2 and historically was not compacted, leading to fragmentation.
What lands on LOH
- Large
byte[](file buffers, network reads). - Long strings (think 50 KB+ of JSON).
- Large 2-D arrays.
Why it matters
- Fragmentation. Allocate / free big buffers of varying sizes — holes the allocator cannot reuse — working set grows even though heap is mostly empty.
- Gen 2 GC cost. Every LOH alloc pushes pressure into Gen 2, which is the most expensive collection.
Three production fixes
1. ArrayPool
byte[] buf = ArrayPool<byte>.Shared.Rent(64 * 1024);
try { ... } finally { ArrayPool<byte>.Shared.Return(buf); }
Pooled buffers are reused — most servers see allocation rate drop by an order of magnitude.
2. RecyclableMemoryStream
Drop-in replacement for MemoryStream that pools its internal buffers:
private static readonly RecyclableMemoryStreamManager Mgr = new();
using var ms = Mgr.GetStream();
await source.CopyToAsync(ms);
3. Force LOH compaction (rare, last resort)
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
Use sparingly — pauses are long. Better to fix the allocation pattern.
How to detect LOH pressure
dotnet-counters monitor --counters System.Runtime— watchgen-2-gc-countandloh-size.- PerfView — "GC Stats" report — look at "% Time in GC" and LOH allocation rate.
- BenchmarkDotNet
[MemoryDiagnoser]shows per-method allocations.
Common offenders found in real codebases
| Pattern | Fix |
|---|---|
JsonSerializer.SerializeToUtf8Bytes(largeObj) | Stream API + Utf8JsonWriter |
File.ReadAllBytes then process | FileStream.CopyTo(pooledBuffer) |
string.Concat on many large strings | StringBuilder with capacity hint, or string.Create |
Returning new byte[Length] from a method | Accept Span<byte>/Memory<byte> parameter, write in place |
.NET 8+ improvements
The LOH is compactable now via the standard background GC, but the cost is still high — design to avoid LOH allocations in the steady state.