Memory & Garbage Collection · Lesson 6

CoreCLR heap regions: SOH, LOH, and POH

Separate payload size from runtime-specific heap organization and public generation reporting.

Explore the interactive Memory Lab

Change owners, step through generations, pinning and finalization, or compare memory under load. The model explains concepts; the SDK examples measure real runtime observations.

Open the Memory Lab ↗

The memory contract

CoreCLR separates ordinary small-object allocation from large-object and specially pinned-object organization. SOH, LOH, and POH describe allocation regions with different management concerns; they are not three new language-level object types. The default CoreCLR large-object threshold is about eighty-five thousand bytes of object size, including overhead, and configuration can change that threshold. Do not equate an array's element count with total object size or apply a desktop threshold automatically to another managed runtime. This lesson qualifies the architectural names before making any claims about a particular target.

Trace the actual program

The sample creates a byte array with 1,024 payload elements and another with 90,000. It prints small-bytes:1024 and large-bytes:90000, then deliberately keeps both reachable. These lengths are deterministic properties of the program's arrays, not measurements proving where either runtime placed them. The labels describe relative payload sizes. In a CoreCLR discussion the larger allocation is relevant to the usual LOH threshold, but the browser run still reports only lengths. No address, heap segment, physical generation, or resident-memory change is observed by this program.

How the mechanism works

Large-object management trades the cost of moving large blocks against fragmentation and collection overhead. CoreCLR normally manages LOH space without compacting every ordinary collection, while supported policies can request compaction in appropriate circumstances. Its pinned-object heap supports designated pinned allocations so they do not disrupt the movable small-object regions in the same way. Pinning an already existing object with a handle does not migrate it into POH. Public GC.GetGeneration is not sufficient to distinguish all of these physical arrangements, especially across runtimes with different collectors.

Boundaries and misconceptions

The threshold uses total allocation size, so a general rule saying an array with exactly eighty-five thousand elements always crosses one universal boundary is imprecise. Element width, headers, alignment, runtime configuration, and collector implementation matter. Likewise, calling LOH a physical generation in an implementation discussion does not mean public callers should expect GC.MaxGeneration to become three. Keep the public API contract, CoreCLR internals, and the actual browser profile separate. A length-only sample cannot establish fragmentation or prove that an object has been compacted.

Practice: change one ownership path

First change the smaller payload length to 2,048 and predict only its printed payload count changes. Hint: inspect the array's Length rather than inventing a heap label from the number. Then replace byte elements with a small integer array and compare element count with the logical bytes needed for its data. Explain why the complete object still includes runtime overhead, and why these calculations do not replace a target-aware heap inspection when physical allocation behavior is the question.

Practice: test a second boundary

Next run the unchanged source under a documented desktop CoreCLR and under SharpRune. Record the identical payload lengths and the different runtime context. Hint: a portable language-level observation can remain stable even when heap-region implementation differs. Design a desktop investigation using supported heap tools to inspect the larger allocation, but do not present those desktop observations as the browser's results. If the application needs many large temporary buffers, evaluate a bounded reuse policy and measure it instead of treating the LOH label alone as evidence of a performance defect.

Review the evidence

The architecture vocabulary helps formulate questions: how large are allocations, how long do they survive, are they pinned, and which collector manages them? The actual answer requires the target profile and appropriate evidence. For this sample, retain the two exact length lines and the explicit liveness boundary. When reviewing a memory recommendation, require its runtime assumptions before accepting threshold rules or compaction advice. That discipline prevents useful CoreCLR documentation from becoming an inaccurate universal account of every environment capable of running C#.

  • The printed values are payload lengths, not heap-region measurements.
  • CoreCLR thresholds include object size and can be configured.
  • Pinning an existing object does not relocate it into POH.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Buffers;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

byte[] small = new byte[1024];
byte[] large = new byte[90_000];
Console.WriteLine($"small-bytes:{small.Length}");
Console.WriteLine($"large-bytes:{large.Length}");
GC.KeepAlive(small);
GC.KeepAlive(large);

Expected output

small-bytes:1024
large-bytes:90000
Try this
The printed values are payload lengths, not heap-region measurements.

Further reading