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() or MemoryStream holding the entire payload with a FileStream or partitioned buffer reads.
  • Deserialize large JSON or XML payloads directly from a stream using APIs like System.Text.Json.JsonSerializer.DeserializeAsync rather than reading the entire content into a string first.

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.