Memory & Garbage Collection · Lesson 31

GC modes, DATAS and container budgets

Read effective runtime settings and distinguish a managed heap limit from a whole-process memory limit.

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 ↗
.NET 10 SDK / desktop CoreCLR

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

Several independent policies share the GC name

Workstation and server GC, background collection and adaptive heap policies describe related but different runtime choices. Server GC is not simply background GC turned on, and background collection does not mean application threads can never pause. The useful question is which effective configuration serves a particular workload under its actual CPU and memory constraints. Reading a project setting without checking the deployed runtime and host can miss important differences.

The portable program reports observations rather than prescribing settings. SharpRune's bundled Mono/WebAssembly execution environment is not desktop CoreCLR, and Unity's documented collector is another implementation again. Runtime-independent ownership lessons transfer well; concrete heap layout, tuning knobs and diagnostic tool availability do not automatically transfer with them. Keep platform claims attached to the environment in which they were tested.

Adaptive heap sizing and DATAS

Dynamic adaptation to application sizes is a CoreCLR policy intended to make heap sizing respond to application characteristics, including long-lived data. Current .NET documentation states that DATAS is enabled by default starting with .NET 9. Treat that as a versioned policy fact, not a universal property of every CLR, historical release or managed runtime. An explicit deployment override and effective runtime version still matter.

Adaptive sizing does not eliminate a memory budget or make poor ownership harmless. A cache that retains every response remains a growing live set. More aggressive memory conservation can trade throughput or collection work against retained capacity. Compare modes with the same input distribution, concurrency, warmup and observation window. A smaller heap in an idle demonstration is not sufficient evidence that a production service meets its latency objective under bursts.

Container limits are not managed ownership limits

A managed heap hard limit concerns GC heap policy; it is not a cap on every allocation in the process. Native libraries, stacks, runtime metadata, code and other resident memory need space too. The container or operating system can enforce a broader process or group limit. Leave explicit headroom and measure peak combined behavior rather than allocating the entire quota to the managed heap.

Configuration syntax is also part of correctness. Runtime configuration files and environment variables can use different representations for numeric settings. Verify the documented unit and parsing rules for the exact knob rather than copying an unqualified number from an unrelated example. Some settings apply at startup and require a new process. Changing an environment variable in a running application does not establish that the collector adopted a new policy.

Trace effective observations

The portable example prints framework description, whether server GC is active, the reported maximum generation and processor count. These values can differ across the app runtime and desktop SDK. Their variation is expected, so the lesson intentionally has no fixed expected transcript. The final line labels the output as runtime observations instead of inventing one desktop answer for every platform.

Processor count is a runtime observation under the current host environment, not proof of how many physical cores are available exclusively to the service. Likewise IsServerGC tells you the effective GC flavor, not whether that choice is optimal. A maximum generation value does not identify the physical region of an arbitrary object or describe Unity's collector. Keep the meaning of each printed value narrow and explicit.

Run a quota-aware experiment

The SDK program records CoreCLR framework, effective server mode, visible processors, latency mode and GCMemoryInfo values after a bounded educational collection. It does not modify the machine's container settings, GC policy or startup environment. Run separate processes with deliberately chosen startup configurations when comparing modes, and record those configurations alongside results.

A realistic comparison includes steady traffic, a burst, downstream slowness and an idle recovery interval. Track completed throughput, tail latency, allocation rate, pause time, retained live bytes, committed GC bytes and process memory. Record CPU quota and memory quota because a configuration that works on a development laptop can behave differently in a constrained container. Never change several GC knobs and the application workload simultaneously if you want a useful causal explanation.

Design a deployment comparison

Keep application code and data fixed while changing one startup policy. Use the same runtime patch version and representative limits, and allow comparable warmup. Validate output correctness before comparing performance. Set an acceptance condition that includes both memory headroom and user-visible latency. Roll back if the experiment violates the workload objective even when a favorite memory chart improves.

For an interview, explain why enabling server GC does not prove that a container cannot run out of memory. Distinguish managed retention, collector sizing and nonmanaged memory. Mention that replicas multiply baseline costs and that averages can hide one hot instance. A strong answer proposes measurements under the actual quota rather than presenting one magic environment variable as a complete capacity plan.

Practice and review

Run the portable observation on SharpRune and the SDK observation on desktop CoreCLR. List which statements are shared ownership principles and which are platform observations. Do not compare unlike counters as though they were a benchmark of which runtime is better.

Read the official settings for one chosen deployed runtime version and write down its name, default, unit, startup behavior and effective verification method. Build a small comparison matrix for normal load and a slow dependency. Finish by reserving native and transient headroom explicitly and explaining why a managed heap limit cannot replace a whole-process capacity model or an application admission policy.

.NET 10 SDK / desktop CoreCLR

GC modes, DATAS and container budgets — CoreCLR experiment

Set up the SDK project

  1. Install .NET 10 SDK and unzip the project into a new directory.
  2. Read README.md and run dotnet run -c Release --project MemoryLab.csproj in this isolated process.
  3. Record the runtime, effective configuration and full output. Repeat the same workload; numeric observations are not universal expected values.

Download complete SDK project ↗

Project files

MemoryLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>
Program.csDownload file ↗
using System.Runtime;
using System.Runtime.InteropServices;
Console.WriteLine($"framework:{RuntimeInformation.FrameworkDescription};server:{GCSettings.IsServerGC};cpu:{Environment.ProcessorCount};latency:{GCSettings.LatencyMode}");
GC.Collect(2, GCCollectionMode.Forced, blocking: true);
var info = GC.GetGCMemoryInfo();
Console.WriteLine($"available:{info.TotalAvailableMemoryBytes};high-threshold:{info.HighMemoryLoadThresholdBytes};committed:{info.TotalCommittedBytes}");
Console.WriteLine("DATAS and limits are startup policy; this program does not change host/container settings.");
README.mdDownload file ↗
# GC modes, DATAS and container budgets

Environment: .NET 10 SDK / desktop CoreCLR

Run `dotnet run -c Release --project MemoryLab.csproj` with .NET 10 SDK.

This is an isolated desktop CoreCLR experiment. It may deliberately request bounded collections for observation. Never add these forced collection calls to a production request path. Numeric counters, generations, addresses and RSS vary. The portable SharpRune program is separate.

Primary references:
https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector
https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/workstation-server-gc
https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/background-gc

Expected result

The bounded program exits successfully and prints genuine CoreCLR observations. Counts, generations, heap sizes and timings vary; compare ownership invariants and matching observation windows rather than hardcoded measurements.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System.Runtime;
using System.Runtime.InteropServices;
Console.WriteLine($"framework:{RuntimeInformation.FrameworkDescription}");
Console.WriteLine($"server-gc:{GCSettings.IsServerGC}");
Console.WriteLine($"max-generation:{GC.MaxGeneration}");
Console.WriteLine($"processors:{Environment.ProcessorCount}");
Console.WriteLine("runtime-observation:values-vary");
Runtime-dependent observation

Generation counts and allocation measurements can vary. Follow the lesson’s observation steps rather than matching a fixed transcript.

Open C# practice

Try this
Trace ownership before inferring collection. These are runtime observations; do not compare fixed numeric values.

Further reading

Interview questions for this lesson