Large Object Heap in C#: How to Avoid LOH Fragmentation
This article provides an overview of the Large Object Heap (LOH) in
.NET, explaining how it works and why it is prone to memory
fragmentation. It explores the operational behavior of the Garbage
Collector (GC) regarding large memory allocations, the risks of
fragmentation leading to OutOfMemoryException errors, and
actionable patterns and techniques developers can implement to eliminate
or reduce LOH fragmentation in production C# applications.
What is the Large Object Heap (LOH)?
In .NET, the Garbage Collector partitions managed memory into distinct generations (Generation 0, 1, and 2) to optimize allocation and collection performance. Small, short-lived objects live in Generations 0 and 1. However, when an object is 85,000 bytes or larger (or a double array with 1,000 or more elements on 64-bit platforms), the runtime allocates it directly onto a dedicated segment called the Large Object Heap.
The LOH is collected exclusively during a full Generation 2 collection. Because copying and moving tens or hundreds of megabytes of contiguous memory is computationally expensive, the GC historically never compacted the LOH during garbage collection; it simply swept it, marking the dead memory addresses as free space for future allocations.
What Causes LOH Fragmentation?
Fragmentation occurs when an application frequently creates and disposes of large objects with varying lifespans and sizes. When these objects are collected, they leave gaps of free memory between surviving objects.
If a new 100 KB object needs allocation, but the available free space
is fragmented into non-contiguous 40 KB blocks, the GC cannot fit the
new object into those spaces. Consequently, the runtime is forced to
allocate additional memory segments from the operating system, inflating
memory consumption. Over time, an application can throw an
OutOfMemoryException despite having plenty of total free
memory, simply because no single contiguous block is large enough to
satisfy the allocation request.
How to Avoid LOH Fragmentation
To maintain low memory usage and avoid performance degradation, developers should focus on minimizing allocations on the LOH and reusing existing buffers.
1. Reuse Buffers with
ArrayPool<T>
The most effective way to eliminate transient LOH allocations is to
reuse large arrays using the System.Buffers namespace.
Instead of allocating a new byte[] for every I/O operation,
rent an array from the pool and return it when finished:
using System.Buffers;
public void ProcessLargeData(int bufferSize)
{
// Rent an array from the shared pool
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
// Use the buffer (remember it may be larger than bufferSize)
ProcessBuffer(buffer, bufferSize);
}
finally
{
// Return the array to the pool for reuse
ArrayPool<byte>.Shared.Return(buffer);
}
}2. Stream Data Instead of Loading Full Payloads
Avoid reading massive payloads, files, or API responses into single contiguous structures such as strings or byte arrays. Use chunked streaming instead:
- Replace
File.ReadAllBytes()orMemoryStreamholding the entire payload with aFileStreamor partitioned buffer reads. - Deserialize large JSON or XML payloads directly from a stream using
APIs like
System.Text.Json.JsonSerializer.DeserializeAsyncrather than reading the entire content into astringfirst.
3. Break Up Large Collections
If an application requires storing massive amounts of data in memory,
avoid monolithic single-array containers. A collection such as
List<T> relies on an internal array that doubles in
size when capacity is exceeded, often promoting the array to the
LOH.
Instead, consider chunked data structures, such as a linked list of
smaller arrays (e.g., List<byte[]>, where each byte
array is kept below the 85,000-byte threshold), preventing the GC from
routing the collections to the LOH.
4. Enable LOH Compaction On Demand
In modern .NET (.NET Core and .NET 5+), you can explicitly request that the GC compact the LOH during the next full collection. This should not be done routinely due to performance pauses, but it can be useful during low-traffic maintenance windows or after large, irregular batch processing jobs:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(2, GCCollectionMode.Forced, true, true);5. Leverage the Pinned Object Heap (.NET 5+)
Pinned buffers previously had to reside on the LOH or Gen 0/1/2,
worsening fragmentation because the GC cannot move pinned memory.
Starting in .NET 5, native interop and socket buffers can be allocated
on the Pinned Object Heap (POH) using
GC.AllocateArray<T>(..., pinned: true). This isolates
pinned allocations away from the standard LOH, reducing fragmentation in
high-throughput network or I/O applications.