Interview Handbook by Sayed Elmahdy العربية

Topic 03 · C# & .NET

.NET runtime: CLR, JIT & GC

.NET Framework vs .NET 10, CLR and IL, JIT types, tiered PGO, AOT, GC, IDisposable, memory leaks.

18 questionsJunior · 6Mid · 7Senior · 510 quiz

Concepts

What you will learn: When a application runs, what happens to its code, how is memory managed, and why do deployment choices and runtime settings matter? We will move from platform versions and compilation through , cleanup and deployment, tying each term to its effect on a working application.

Before you start: An application runs inside a process, which has execution resources and memory. Before learning GC details, remember that some data is temporary while other objects remain alive as long as the application needs them. If the terms are new, start with the .NET family card and follow the topic in order.

Select a term with a dotted underline for its meaning, then continue reading from the same place.

#The .NET family and support policy

Choose a platform by deployment needs and upgrade deadlines. Similar names do not make an old application compatible with a new runtime without changes.

PlatformScopePractical decision
Framework 4.xWindows; legacy System.Web, Web Forms and applicationsMaintain according to the Windows and version support policy; assess dependencies before migrating
.NET Core 1–3.1The open-source line targeting Windows, Linux and macOSThese releases are out of support
.NET 5+Continuation of .NET Core under one name; workloads such as WPF remain Windows-onlyChoose a supported release, not merely a number above 5
.NET 10 released November 2025; includes C# 14Supported through November 2028

Even releases are LTS with three years of support; odd releases are . Starting with .NET 9, STS support is 24 months. Both .NET 8 and .NET 9 reach end of support in November 2026. Staying supported also requires applying servicing and security updates.

dotnet --info
dotnet --list-sdks
dotnet --list-runtimes

The commands inspect the environment, installed build tools and installed runtimes respectively. An on a developer machine says nothing about a server's installed runtime.

#From C# to IL: CLR, CTS and CLS

A worked example
  1. Start with a C# method adding two numbers. A typical build produces and type metadata in an assembly, rather than simply storing the original source text.
  2. The manages execution, memory and exceptions. On the path, the method's instructions are translated into machine code when needed.
  3. Follow the stages: source, build output containing IL, runtime, machine code. Native uses a different path that moves compilation to publishing time.

Languages need a shared representation of code and types to exchange libraries. Think of IL as shared instructions and the runtime as the execution manager that translates them and manages resources.

C# source
  -> Roslyn compiler
  -> Assembly: IL + metadata + manifest
  -> Runtime loader and type system
  -> JIT -> native instructions -> CPU
           GC, exceptions, threading and interop
  • CLR: Common Language Runtime, often used to describe execution services. CoreCLR is a principal modern runtime; other runtimes serve other workloads.
  • IL: Intermediate Language, not instructions. Portability also depends on libraries and native dependencies.
  • : Common Type System, defining how types are represented and used, including value and reference types.
  • : Common Language Specification, a common subset of rules for public usable across .NET languages. Unsigned types, for example, are not CLS-compliant.
Managed execution is not a security sandbox. Unsafe code and interop can access native memory and introduce serious bugs.
[assembly: CLSCompliant(true)]
public static class InvoiceMath
{
    public static decimal AddTax(decimal total) => total * 1.14m;
}

The attribute asks the compiler to check the public API against CLS rules; it does not make native dependencies cross-platform.

#Classic JIT types and their modern equivalents

The three- question uses historical terminology. Name the three, then explain that modern does not expose them as three selectable operating modes.

NameHistorical ideaToday
Pre-JITGenerate native code before execution, as with Framework's NGen and Native address related needs with different designs
Econo-JITSimple on-demand compilation, with native code discardable to save memoryObsolete; not an option in .NET 10
Normal-JITCompile on first use and retain the native codeThe underlying idea continues with tiering and
# Alternatives: publish the same project in different modes.
dotnet publish -c Release -r linux-x64 --self-contained false
dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true
dotnet publish -c Release -r linux-x64 -p:PublishAot=true

The commands select ordinary JIT deployment, R2R and Native AOT, provided the project and dependencies support AOT. NGen and Native AOT are not the same implementation.

#Tiered compilation, OSR and dynamic PGO

Optimizing every method before the first request delays startup; keeping all code minimally optimized hurts . The runtime balances the two in stages.

  1. Tier 0 provides quickly generated code; can supply initial precompiled code.
  2. The runtime observes usage and recompiles hot methods at Tier 1 with stronger optimizations.
  3. On-Stack Replacement can transfer a running long loop into optimized code.
  4. Dynamic collects actual branch and type profiles for inlining and guarded devirtualization. It is enabled by default from 8 in typical configurations.
<PropertyGroup>
  <TieredCompilation>true</TieredCompilation>
  <TieredPGO>true</TieredPGO>
</PropertyGroup>

The settings enable tiering and profile guidance respectively; they illustrate the controls rather than a required project change. No fixed promotion call count is guaranteed. Measure startup separately from steady-state behavior; a single cold call can mislead.

#Choosing JIT, ReadyToRun or Native AOT

Choose publishing mode according to cold starts, memory and compatibility, rather than novelty.

Native
CompilationAt runtimeMuch code at publish time, with and JIT fallbackAt publish time; no application JIT
StartupPays JIT costReduces initial JIT workOften suitable for cold starts; measure the application
Warm executionDynamic and runtime specializationHot code may be re-JIT compiledNo dynamic JIT PGO
SizeDepends on deploymentOften larger from IL plus native codeDepends on trimming and dependencies; not always smaller
ConstraintsGreater dynamic-code flexibilitySimilar flexibility to JITNo runtime code generation; needs preserved metadata
Native AOT links and other runtime services into the application, without requiring an installed shared runtime. Not every workload supports it; evaluate AOT with Minimal .

#Stack and heap: storage versus semantics

Value versus reference explains copying; stack versus heap explains storage and lifetime. These are different questions. A thread's stack is working space, while the managed heap holds objects whose lifetimes need not follow a single call.

public struct Quantity { public int Value; }
public sealed class OrderLine { public Quantity Quantity; }

var line = new OrderLine();                 // (1)
line.Quantity.Value = 2;
var copy = line.Quantity;                   // (2)
copy.Value = 9;
object boxed = copy;                        // (3)
Console.WriteLine(line.Quantity.Value);     // 2
🧾 Code walkthrough
  1. (1) Quantity is stored inline inside OrderLine, without a separate object .
  2. (2) Struct assignment copies the value. The may keep the local in a register; locals are not guaranteed stack slots.
  3. (3) creates a managed object containing a copy.

References themselves may be locals or fields. Closures and async state machines can move local state onto the heap. Choosing a struct does not guarantee allocation-free code.

#GC generations, LOH and background collection

A worked example
  1. An request creates a temporary list. While a live reference reaches it, the collector must treat it as reachable and retain it.
  2. After all live references disappear, it may become collectible; collection is not guaranteed immediately. Gen0, Gen1 and Gen2 organize work around objects surviving collections.
  3. Keeping the list unnecessarily in a static field can retain it indefinitely. Having a does not prevent accidental retention: find what holds the reference.

Most temporary objects die quickly, so frequent small collections are cheaper than repeatedly scanning the whole heap. GC reachability from roots such as live locals, static fields and GC handles.

  • Small objects typically start in Gen0. Survivors may move to Gen1 and Gen2. A Gen2 collection includes younger generations.
  • The holds large objects, normally at least 85,000 bytes including object overhead. It is collected with Gen2 and generally is not compacted by default.
  • Background GC performs much Gen2 work concurrently with application execution; it still has pauses and stop-the-world phases.
  • Compaction can move objects and update references. prevents movement and may increase fragmentation.
byte[] small = new byte[1024];
byte[] large = new byte[100_000];
Console.WriteLine(GC.GetGeneration(small));  // typically 0 if no GC intervenes
Console.WriteLine(GC.GetGeneration(large));  // 2: LOH is collected with Gen2

The first result can change if a collection intervenes. This is an illustration, not an application assertion. Avoid forcing GC.Collect() after requests; address rate and retention first.

#Server GC, workstation GC and DATAS

A high- service has different needs from an interactive desktop application. Workstation suits interactive or resource-constrained workloads. Server GC uses multiple heaps and workers for throughput, potentially consuming more memory.

Dynamic Adaptation to Application Sizes adapts server GC to long-lived data and workload needs, including changing heap counts. Introduced in 8, is enabled by default for server GC from .NET 9. It does not replace memory limits or fix leaks.

<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
  <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>

The settings request server GC and background collection. Hosting and environment affect the actual mode; inspect GCSettings.IsServerGC. Compare and p99 , throughput and memory under the intended container limits. DATAS documentation.

#Dispose, finalizers and SafeHandle

reclaims unreachable managed memory, but a file handle or connection needs predictable release. Dispose provides deterministic cleanup; a finalizer is a fallback with unpredictable timing and no process-exit guarantee.

// An inheritable owner of a managed disposable resource.
public class InvoiceFile : IDisposable
{
    private bool _disposed;
    private readonly FileStream _stream = File.OpenWrite("invoice.txt");

    public void Write(byte[] bytes)
    {
        ObjectDisposedException.ThrowIf(_disposed, this); // (1)
        _stream.Write(bytes);
    }

    public void Dispose()
    {
        Dispose(true);                                  // (2)
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        if (disposing) _stream.Dispose();                // (3)
        _disposed = true;
    }
}

using var file = new InvoiceFile();                      // (4)
file.Write([65, 66]);
🧾 Code walkthrough
  1. (1) Access after cleanup produces a clear exception.
  2. (2) Public Dispose calls the extensibility hook and suppresses finalization if a derived type supplies a finalizer.
  3. (3) Managed resources are disposed only on the deterministic path. Repeated disposal is safe here; the example is not thread-safe.
  4. (4) using guarantees disposal on scope exit, including exceptions.

A sealed class owning only a stream needs a simpler Dispose method. Implementing IDisposable does not require adding a finalizer. For raw native handles, prefer SafeHandle and put native release logic in ReleaseHandle; the owner disposes the SafeHandle. In the older pattern that directly owns a raw resource, a finalizer calls Dispose(false) without accessing managed objects. Use IAsyncDisposable and await using for asynchronous cleanup.

#Memory leaks, assemblies and deployment units

A managed leak consists of reachable objects the application no longer needs: unbounded caches, subscriptions to long-lived publishers, timers, or singletons retaining scoped services. Native leaks can increase working set without increasing the managed heap.

  1. Observe memory under stable load and after idle time; distinguish working set from live managed heap.
  2. Inspect rate, Gen2, and collection counters.
  3. Compare gcdumps captured at two points and inspect retained types and reference paths. Use a process dump and gcroot when deeper root analysis is needed.
  4. Fix ownership or lifetime, then repeat the scenario. Dump capture can pause the process and expose sensitive data.
# Install the diagnostic tools separately if needed.
dotnet-counters monitor --process-id 1234 --counters System.Runtime
dotnet-gcdump collect --process-id 1234 --output orders.gcdump
dotnet-gcdump report orders.gcdump

The commands monitor runtime metrics, capture a managed heap graph and print a report. The text report is a starting point; reference-path analysis needs a suitable viewer. dotnet-gcdump reference.

TermMeaning
AssemblyA unit of types, metadata and manifest, commonly a ; not a NuGet package
NuGet packageA distribution unit containing assemblies for target frameworks, possibly native assets and build targets
/ runtimeThe SDK builds and supplies tooling; the runtime executes. needs its appropriate shared framework
Framework-dependentA smaller deployment relying on a supported runtime installed on the host
Self-containedIncludes a runtime for a specific and architecture; republish to apply runtime patches
Managed / unmanagedManaged code uses runtime services; native libraries manage their own memory contracts

Self-contained does not mean single-file or Native . P/Invoke connects managed code to native and requires correct type and ownership contracts.

Interview questions

Check your reasoning

Why does this choice fit? When would it fail? What changes under higher load or a dependency failure?

3.1How do .NET Framework, .NET Core and modern .NET differ?Junior
Model answer

I distinguish the original Windows-only Framework from the open-source, cross-platform .NET Core line. Starting with .NET 5, that line became modern .NET, including .NET 10. For new services I normally choose a supported modern release. For an existing Framework application I first check dependencies, especially System.Web and Windows-specific components. A shared language does not make migration automatic, and workloads such as WPF remain Windows-specific.

3.2What do LTS and STS mean for a deployment decision?Junior
Model answer

I use the support window to plan upgrades and security maintenance. Even-numbered releases are with three years of support; odd-numbered releases are , extended to 24 months starting with 9. .NET 10 is supported through November 2028, while .NET 8 and .NET 9 both end support in November 2026. I still install servicing updates throughout that window; choosing LTS does not remove patching responsibilities.

3.3What happens between compiling C# and executing it?Junior
Model answer

In a normal deployment, Roslyn compiles C# into and metadata inside an assembly. The runtime loads the assembly and resolves types, then the JIT translates methods into native instructions as needed. Later calls reuse compiled code, though tiered compilation can replace it with optimized versions. I also mention that and Native move compilation work earlier, so first-call JIT is not a universal description of every deployment.

3.4What is the difference between CTS and CLS?Junior
Model answer

I describe as the common type system: it defines how types are declared, represented and used. is a smaller set of interoperability rules for consumed by different .NET languages. For example, an API exposing unsigned types can be valid C# without being CLS-compliant. When publishing a broadly reusable library, I can request CLS checks on its public surface instead of assuming every language supports every C# feature.

3.5Are value types always stored on the stack?Junior
Model answer

No. I separate value semantics from storage. A struct field is stored inline in its containing object, which may be on the managed heap. A local struct might be on a thread's stack or optimized into registers. puts a copy into a managed object, and captured or asynchronous local state may live on the heap. The useful guarantee is that ordinary value assignment copies the value, not that every struct avoids heap memory.

3.6What is an assembly, and how does it differ from a NuGet package?Junior
Model answer

I treat an assembly as a runtime and metadata unit containing types and a manifest, usually in a . A NuGet package is a distribution unit used during restore and build. It may contain several assemblies for different target frameworks, native binaries and build assets. One package therefore does not necessarily mean one assembly. I check target-framework compatibility and transitive dependencies during package selection rather than relying only on the package name.

3.7What are Pre-JIT, Econo-JIT and Normal-JIT?Mid
Model answer

I would frame these as historical terms. Pre- meant generating native images before execution, commonly associated with NGen in Framework. Econo-JIT traded optimizations and retained code for lower memory use and is obsolete. Normal-JIT compiles methods on demand and retains native code. Modern .NET builds on that idea with tiering and dynamic . and Native address ahead-of-time compilation needs, but they are distinct technologies rather than renamed NGen modes.

3.8How do tiered compilation, OSR and dynamic PGO work together?Mid
Model answer

I see tiered compilation as a startup- compromise. Initial code is available quickly, then hot methods are recompiled with stronger optimizations. allows a long-running loop to move into optimized code without waiting for another method call. Dynamic collects actual execution information, including frequently observed types and branches, and uses it to improve optimized code. That is why I separate cold-start measurements from warmed-up performance and avoid quoting a fixed promotion threshold.

3.9How does the generational GC decide what to collect?Mid
Model answer

The follows references from roots and reclaims objects it cannot reach; it is not reference counting, so unreachable cycles can be collected. Generations exploit the observation that many objects die young. Small usually start in Gen0, and survivors can move to Gen1 and Gen2. Older collections include younger generations. I focus on allocation rate and retained live data, because repeatedly allocating short-lived objects and retaining an unbounded cache are different problems requiring different fixes.

3.10What is the LOH and why can large buffers cause problems?Mid
Model answer

The Large Object Heap normally receives objects of at least 85,000 bytes, including overhead, and is collected with Gen2. Because it is generally not compacted by default, repeated large temporary can increase collection pressure and fragmentation. I first consider streaming or smaller buffers, then ArrayPool when measurement supports it. A pool is not free memory: it retains arrays, may return a larger array, and requires returning each rental only after all consumers finish.

3.11Does background GC mean the application never pauses?Mid
Model answer

No. Background overlaps much of a Gen2 collection with application execution, but coordination and some collection phases still suspend managed execution. Younger collections may also occur while background work is active. I distinguish background versus blocking collection from workstation versus server GC; they describe different choices. For -sensitive services, I measure pause distributions and request tail latency under realistic memory limits rather than assuming that background collection eliminates pauses.

3.12Why do we need Dispose if .NET has garbage collection?Mid
Model answer

I use Dispose to release resources at a predictable point, such as returning a connection or closing a file handle. reclaims managed memory on its own schedule and does not automatically call every object's Dispose method. Disposing also does not immediately remove the managed object from the heap. I use using for synchronous cleanup and await using for asynchronous cleanup, and make resource ownership explicit so a consumer does not dispose something owned elsewhere.

3.13How do SDK, runtime, self-contained and framework-dependent relate?Mid
Model answer

The supplies the compiler and build tools; deployment hosts generally need only the appropriate runtime. A framework-dependent app relies on an installed shared runtime. A self-contained app carries a runtime for its target and architecture, which gives deployment control but makes me responsible for republishing runtime patches. I keep that separate from single-file packaging and Native . For an app, I also check that the required ASP.NET Core shared framework is available.

3.14When would you choose Native AOT over ReadyToRun or JIT?Senior
Model answer

I consider Native when cold starts, deployment footprint or memory matter enough to justify its constraints. I audit dependencies, trimming warnings, metadata and dynamic code generation, then test the published binary on the target platform. It still includes and runtime services. is a useful intermediate choice when startup matters but flexibility is needed. For long-running services, warmed-up JIT performance with dynamic may be preferable, so I benchmark the actual workload before choosing.

3.15How would you investigate a service whose memory grows continuously?Senior
Model answer

I first distinguish working-set growth from growth in live managed objects and observe the service under a repeatable load. With dotnet-counters I inspect rate, Gen2 and behavior. I compare heap captures to identify growing types and retention paths, using a fuller dump when root analysis needs it. Common causes include unbounded caches and event subscriptions. If managed memory is stable, I investigate native allocations and mapped memory. After fixing ownership, I repeat the same load to verify the trend.

3.16When should a class have a finalizer, and why prefer SafeHandle?Senior
Model answer

I avoid adding a finalizer just because a class implements IDisposable. If it owns managed disposable objects, I dispose those deterministically. For native handles I prefer SafeHandle, which centralizes release and provides a reliable finalization fallback. A directly owned raw resource may need a finalizer, but that path must avoid managed-object dependencies and must not throw. Finalization timing is unpredictable, adds collection cost, and is not guaranteed at process exit, so it cannot replace using.

3.17What is DATAS, and does it make GC tuning unnecessary?Senior
Model answer

dynamically adapts server to application size, including its long-lived data and heap needs. It became enabled by default for server GC in 9. I would start with the runtime defaults and measure , tail and memory in the actual deployment limits. It does not remove the need for capacity planning or cure leaks. A rooted cache remains rooted regardless of how effectively the collector sizes its heaps.

3.18What changes when managed code calls an unmanaged library?Senior
Model answer

I check the native , parameter sizes, string encoding, calling convention and ownership rules. Marshaling may copy data, and a native function retaining a pointer requires an explicitly valid lifetime. tracks managed objects, not arbitrary native , so I wrap owned handles with SafeHandle where possible and pair allocations with the correct release function. I also check platform-specific binaries and test error paths, because a signature mismatch can corrupt memory rather than produce a useful managed exception.

Test yourself

Quiz answers and completed-topic marks are saved in this browser; they do not sync across devices.

Score: 0 / 0

Which .NET 10 support statement is correct?

10 has three years of support from November 2025.

A struct is a field inside a managed class instance. Where is its data?

Value semantics describe copying; the containing object determines storage.

Which classic JIT type is obsolete?

Econo- is historical terminology, not a 10 setting.

What can OSR improve?

On-Stack Replacement can transfer active execution into optimized code.

Which statement about Native AOT is correct?

changes compilation timing, not managed memory services.

Which collection includes LOH reclamation?

The is collected with Gen2 and generally is not compacted by default.

What does using guarantee on ordinary scope exit, including exceptions?

using provides finally-based cleanup; memory reclamation is separate.

An unbounded static cache retains unused orders. Why can GC not free them?

follows reachability, not business usefulness.

Who applies runtime security updates for a self-contained deployment?

The bundled runtime requires an updated deployment artifact.

What is the best first investigation when working set grows?

Working set includes more than managed objects; measurement directs the investigation.