[{"data":1,"prerenderedAt":706},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-clr-runtime-jit-managed-code":3},{"id":4,"title":5,"body":6,"description":691,"difficulty":692,"extension":693,"framework":521,"frameworkSlug":694,"meta":695,"navigation":696,"order":75,"path":697,"qaPath":698,"seo":699,"stem":700,"subtopic":701,"topic":702,"topicSlug":703,"updated":704,"__hash__":705},"blog\u002Fblog\u002Fdotnet-clr-runtime-jit-managed-code.md","How the .NET CLR Works: JIT, GC, and the Assembly Model",{"type":7,"value":8,"toc":671},"minimark",[9,14,28,32,35,45,53,57,64,85,88,94,97,101,108,127,130,135,142,157,160,164,174,184,188,210,224,231,235,250,276,279,294,298,305,325,329,332,388,395,399,406,413,417,428,432,443,446,480,501,505,508,592,606,609,613,619,641,645,667],[10,11,13],"h2",{"id":12},"why-the-clr-comes-up-in-interviews","Why the CLR comes up in interviews",[15,16,17,18,22,23,27],"p",{},"Senior .NET interviews almost always start here. Understanding the runtime is what\nseparates developers who know how to use C# from developers who understand ",[19,20,21],"em",{},"why"," it\nbehaves the way it does. Performance problems, unexpected memory usage, and subtle\nthreading bugs all trace back to CLR behaviour. If you cannot explain what happens\nbetween writing ",[24,25,26],"code",{},"Console.WriteLine(\"hello\")"," and the CPU executing it, interviewers\nknow the gaps in your mental model.",[10,29,31],{"id":30},"from-c-source-to-native-cpu-instructions","From C# source to native CPU instructions",[15,33,34],{},"The journey a .NET program takes before any instruction reaches the CPU:",[36,37,42],"pre",{"className":38,"code":40,"language":41},[39],"language-text","C# source (.cs)\n   │ csc \u002F Roslyn compiler\n   ▼\nAssembly (.dll \u002F .exe) containing:\n  - IL bytecode          ← language-agnostic instructions\n  - Metadata             ← type descriptions, method signatures\n  - Manifest             ← assembly identity, version, references\n   │\n   │ At runtime: CLR loads the assembly\n   ▼\nJIT Compiler (RyuJIT)\n   │ method-by-method, first time each is called\n   ▼\nNative machine code (x86-64, ARM64…)\n   │ cached for the lifetime of the process\n   ▼\nCPU execution\n","text",[24,43,40],{"__ignoreMap":44},"",[15,46,47,48,52],{},"The key insight is the intermediate step: .NET compilers do not produce native code.\nThey produce ",[49,50,51],"strong",{},"IL (Intermediate Language)"," — a stack-based, CPU-agnostic bytecode\nthat the CLR understands. This is what makes .NET polyglot: C#, F#, and VB.NET all\ncompile to the same IL, and can call each other's assemblies without any wrappers.",[10,54,56],{"id":55},"what-il-looks-like","What IL looks like",[15,58,59,60,63],{},"IL is a low-level, typed, stack-based instruction set. Every operation pushes or pops\nfrom the evaluation stack. You rarely read IL directly, but tools like ",[24,61,62],{},"dotnet-ildasm",",\nILSpy, or dnSpy let you inspect it.",[36,65,69],{"className":66,"code":67,"language":68,"meta":44,"style":44},"language-csharp shiki shiki-themes github-light github-dark","\u002F\u002F C# source:\nstatic int Add(int a, int b) => a + b;\n","csharp",[24,70,71,79],{"__ignoreMap":44},[72,73,76],"span",{"class":74,"line":75},"line",1,[72,77,78],{},"\u002F\u002F C# source:\n",[72,80,82],{"class":74,"line":81},2,[72,83,84],{},"static int Add(int a, int b) => a + b;\n",[15,86,87],{},"The compiler emits roughly:",[36,89,92],{"className":90,"code":91,"language":41},[39],".method private hidebysig static int32 Add(int32 a, int32 b) cil managed\n{\n  ldarg.0    \u002F\u002F push 'a' onto evaluation stack\n  ldarg.1    \u002F\u002F push 'b' onto evaluation stack\n  add        \u002F\u002F pop two values, push their sum\n  ret        \u002F\u002F return top of stack\n}\n",[24,93,91],{"__ignoreMap":44},[15,95,96],{},"This IL is the same regardless of whether you are running on Windows x64, Linux ARM64,\nor macOS. The JIT compiler translates it to the platform-appropriate native instructions\nat runtime.",[10,98,100],{"id":99},"the-jit-compiler-ryujit","The JIT compiler — RyuJIT",[15,102,103,104,107],{},".NET 5+ uses ",[49,105,106],{},"RyuJIT"," as the single cross-platform JIT compiler. When a method is\nfirst called, the CLR's JIT stub is invoked, which:",[109,110,111,115,118,121,124],"ol",{},[112,113,114],"li",{},"Reads the IL for that method from the assembly",[112,116,117],{},"Verifies it for type safety and stack consistency",[112,119,120],{},"Generates native code for the current CPU architecture",[112,122,123],{},"Patches the call site so future calls go directly to native code",[112,125,126],{},"Caches the native code for the process lifetime",[15,128,129],{},"The second call to the same method incurs zero JIT cost — it goes directly to the\ncached native code.",[131,132,134],"h3",{"id":133},"tiered-compilation","Tiered compilation",[15,136,137,138,141],{},"Since .NET Core 3.0, the JIT uses ",[49,139,140],{},"tiered compilation"," to balance startup speed\nwith peak throughput:",[143,144,145,151],"ul",{},[112,146,147,150],{},[49,148,149],{},"Tier 0",": JIT compiles quickly with minimal optimisation. Fast startup. The method\nis instrumented with a call counter.",[112,152,153,156],{},[49,154,155],{},"Tier 1",": Once a method crosses a call threshold, it is queued for full recompilation\nwith aggressive optimisations (inlining, loop unrolling, register allocation).\nThis happens on a background thread; the old tier-0 code stays active until tier 1\nis ready, then call sites are patched atomically.",[15,158,159],{},"The result: .NET applications start quickly (tier 0) and reach high throughput on\nhot paths (tier 1) without developer intervention. Tiered compilation is why .NET\nserver applications warm up relatively quickly and then stabilise at their peak performance.",[131,161,163],{"id":162},"readytorun-r2r-and-native-aot","ReadyToRun (R2R) and Native AOT",[15,165,166,169,170,173],{},[49,167,168],{},"ReadyToRun"," (set ",[24,171,172],{},"\u003CPublishReadyToRun>true\u003C\u002FPublishReadyToRun>",") pre-compiles IL to\nnative code at publish time, reducing JIT work on startup. The native code is embedded\nin the assembly alongside the IL, so tier-1 recompilation can still occur at runtime.",[15,175,176,179,180,183],{},[49,177,178],{},"Native AOT"," (",[24,181,182],{},"\u003CPublishAot>true\u003C\u002FPublishAot>",") eliminates the JIT entirely: the\nentire application is compiled to native code ahead of time. No CLR is needed at\nruntime. The trade-off is a larger binary and no runtime profile-guided optimisation.",[10,185,187],{"id":186},"the-common-type-system-and-the-common-language-specification","The Common Type System and the Common Language Specification",[15,189,190,191,194,195,198,199,202,203,205,206,209],{},"The ",[49,192,193],{},"Common Type System (CTS)"," is the ruleset that defines every type the CLR\nunderstands. All .NET languages must compile their types to CTS-compliant types, which\nis what enables cross-language interop. C# ",[24,196,197],{},"int"," = VB.NET ",[24,200,201],{},"Integer"," = F# ",[24,204,197],{}," = CTS\n",[24,207,208],{},"System.Int32",". They are literally the same type.",[15,211,190,212,215,216,219,220,223],{},[49,213,214],{},"Common Language Specification (CLS)"," is a more restrictive subset. It defines\nthe minimum a .NET language must support to interoperate with ",[19,217,218],{},"any other"," CLS-compliant\nlanguage. Public APIs should be CLS-compliant: no ",[24,221,222],{},"uint"," parameters (VB.NET has no\nunsigned integers), no case-only identifier differences, no global functions.",[15,225,226,227,230],{},"Annotate your library with ",[24,228,229],{},"[assembly: CLSCompliant(true)]"," to get compiler warnings\non CLS violations.",[10,232,234],{"id":233},"assemblies-the-unit-of-deployment","Assemblies — the unit of deployment",[15,236,237,238,241,242,245,246,249],{},"An ",[49,239,240],{},"Assembly"," is the compiled, versioned, deployable unit of .NET code. It is\ntypically a ",[24,243,244],{},".dll"," or ",[24,247,248],{},".exe"," file but is more than just a binary:",[143,251,252,258,264,270],{},[112,253,254,257],{},[49,255,256],{},"IL bytecode"," — the compiled code for all types in the assembly",[112,259,260,263],{},[49,261,262],{},"Manifest"," — assembly identity (name, version, culture, public key token), list of\nreferenced assemblies, and list of modules",[112,265,266,269],{},[49,267,268],{},"Type metadata"," — full type descriptions: class names, method signatures, field\nnames, custom attributes — consumed by reflection and the JIT",[112,271,272,275],{},[49,273,274],{},"Resources"," — embedded images, strings, localisation files (optional)",[15,277,278],{},"The manifest is what the CLR's assembly resolver uses to locate the right version of a\ndependency. Strong-named assemblies include a cryptographic public key token in their\nidentity, enabling side-by-side deployment of different versions.",[15,280,281,282,285,286,289,290,293],{},"In modern .NET, ",[49,283,284],{},"NuGet packages"," are the distribution unit; they contain one or more\nassemblies plus metadata. A project reference (",[24,287,288],{},"\u003CProjectReference>",") becomes an assembly\nreference; a package reference (",[24,291,292],{},"\u003CPackageReference>",") resolves to the package's\nassembly or assemblies.",[10,295,297],{"id":296},"garbage-collection-the-generational-model","Garbage collection — the generational model",[15,299,300,301,304],{},"The CLR GC is a ",[49,302,303],{},"tracing, generational, compacting"," collector. It works by:",[109,306,307,313,319],{},[112,308,309,312],{},[49,310,311],{},"Marking"," all objects reachable from GC roots (static fields, stack variables,\nCPU registers, GC handles)",[112,314,315,318],{},[49,316,317],{},"Sweeping"," unreachable objects (reclaiming their memory)",[112,320,321,324],{},[49,322,323],{},"Compacting"," the heap to eliminate fragmentation (sliding live objects together)",[131,326,328],{"id":327},"generations","Generations",[15,330,331],{},"Most objects die young — a local variable created for a single method call lives only\nmicroseconds. The GC exploits this with three generations:",[333,334,335,351],"table",{},[336,337,338],"thead",{},[339,340,341,345,348],"tr",{},[342,343,344],"th",{},"Generation",[342,346,347],{},"Contents",[342,349,350],{},"Collection frequency",[352,353,354,366,377],"tbody",{},[339,355,356,360,363],{},[357,358,359],"td",{},"Gen 0",[357,361,362],{},"Newly allocated objects",[357,364,365],{},"Very frequent (sub-millisecond)",[339,367,368,371,374],{},[357,369,370],{},"Gen 1",[357,372,373],{},"Survived one Gen 0 collection",[357,375,376],{},"Moderate",[339,378,379,382,385],{},[357,380,381],{},"Gen 2",[357,383,384],{},"Long-lived objects",[357,386,387],{},"Rare, most expensive",[15,389,390,391,394],{},"When Gen 0 fills up, a Gen 0 collection runs. Objects that survive are promoted to\nGen 1. A Gen 1 collection collects Gen 0 and Gen 1. A ",[49,392,393],{},"full (Gen 2) collection","\ncollects all generations and compacts the heap — this is the most expensive and causes\nthe most noticeable pauses.",[131,396,398],{"id":397},"large-object-heap","Large Object Heap",[15,400,401,402,405],{},"Objects larger than ~85 KB are allocated on the ",[49,403,404],{},"Large Object Heap (LOH)",", which is\ncollected with Gen 2 but not compacted by default (compacting large objects is\nexpensive). LOH fragmentation is a common source of memory growth in long-running\nservices that repeatedly allocate large byte arrays.",[15,407,408,409,412],{},"Fix: use ",[24,410,411],{},"ArrayPool\u003Cbyte>.Shared"," to rent and return large buffers rather than\nallocating new ones on every request.",[131,414,416],{"id":415},"background-gc","Background GC",[15,418,419,420,423,424,427],{},".NET's default ",[49,421,422],{},"Server GC"," and ",[49,425,426],{},"Workstation GC"," both support background GC, where\nthe Gen 2 collection runs on dedicated background threads, allowing your application\nthreads to continue running concurrently for most of the collection.",[10,429,431],{"id":430},"managed-vs-unmanaged-code","Managed vs unmanaged code",[15,433,434,435,438,439,442],{},"Code running under CLR supervision is ",[49,436,437],{},"managed"," — the runtime handles memory,\ntype safety, and exception propagation. Code that runs directly on the OS without CLR\noverhead is ",[49,440,441],{},"unmanaged"," — typically native C\u002FC++ libraries or OS APIs.",[15,444,445],{},".NET interacts with unmanaged code through:",[143,447,448,461,467],{},[112,449,450,453,454,456,457,460],{},[49,451,452],{},"P\u002FInvoke (Platform Invocation Services)"," — calling C functions in ",[24,455,244],{},"\u002F",[24,458,459],{},".so"," files",[112,462,463,466],{},[49,464,465],{},"COM Interop"," — calling or hosting COM components",[112,468,469,472,473,476,477,479],{},[24,470,471],{},"unsafe"," ",[49,474,475],{},"blocks"," — pointer arithmetic within managed code (CLR still runs but\nsafety checks are suspended for the ",[24,478,471],{}," region)",[36,481,483],{"className":66,"code":482,"language":68,"meta":44,"style":44},"\u002F\u002F P\u002FInvoke example — calling a Win32 API directly:\n[DllImport(\"kernel32.dll\", SetLastError = true)]\nstatic extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);\n",[24,484,485,490,495],{"__ignoreMap":44},[72,486,487],{"class":74,"line":75},[72,488,489],{},"\u002F\u002F P\u002FInvoke example — calling a Win32 API directly:\n",[72,491,492],{"class":74,"line":81},[72,493,494],{},"[DllImport(\"kernel32.dll\", SetLastError = true)]\n",[72,496,498],{"class":74,"line":497},3,[72,499,500],{},"static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);\n",[10,502,504],{"id":503},"net-framework-vs-net-core-vs-modern-net",".NET Framework vs .NET Core vs modern .NET",[15,506,507],{},"This comes up in almost every .NET interview:",[333,509,510,525],{},[336,511,512],{},[339,513,514,516,519,522],{},[342,515],{},[342,517,518],{},".NET Framework",[342,520,521],{},".NET Core",[342,523,524],{},".NET 5+",[352,526,527,541,554,567,581],{},[339,528,529,532,535,538],{},[357,530,531],{},"Released",[357,533,534],{},"2002",[357,536,537],{},"2016",[357,539,540],{},"2020",[339,542,543,546,549,552],{},[357,544,545],{},"Platform",[357,547,548],{},"Windows only",[357,550,551],{},"Cross-platform",[357,553,551],{},[339,555,556,559,562,565],{},[357,557,558],{},"Open source",[357,560,561],{},"No",[357,563,564],{},"Yes",[357,566,564],{},[339,568,569,572,575,578],{},[357,570,571],{},"Status",[357,573,574],{},"Maintenance only (v4.8.1 final)",[357,576,577],{},"Merged into .NET 5",[357,579,580],{},"Active (annual releases)",[339,582,583,586,588,590],{},[357,584,585],{},"Side-by-side versions",[357,587,561],{},[357,589,564],{},[357,591,564],{},[15,593,594,597,598,601,602,605],{},[49,595,596],{},"Modern .NET"," (5, 6, 7, 8, 9) unified .NET Core and .NET Framework into a single\nplatform. Even-numbered releases (6, 8, 10…) are ",[49,599,600],{},"LTS"," (3-year support); odd-numbered\n(7, 9…) are ",[49,603,604],{},"STS"," (18-month support). For new projects, target the current LTS.",[15,607,608],{},".NET Framework is not dead — Microsoft maintains it for backwards compatibility — but\nit receives no new features. If you are writing a new project or migrating, target\nmodern .NET.",[10,610,612],{"id":611},"reflection-and-source-generators","Reflection and source generators",[15,614,615,618],{},[49,616,617],{},"Reflection"," allows .NET code to inspect type metadata at runtime: enumerate methods\nand properties, read custom attributes, and invoke members by name. It powers DI\ncontainers, ORMs, serialisers, and test frameworks. The downside is performance overhead\nand loss of compile-time safety.",[15,620,621,624,625,628,629,632,633,636,637,640],{},[49,622,623],{},"Source generators"," (C# 9+, Roslyn) generate code at compile time as an alternative\nto runtime reflection. ",[24,626,627],{},"System.Text.Json","'s ",[24,630,631],{},"JsonSerializerContext",", EF Core's compiled\nmodels, and the ",[24,634,635],{},"[GeneratedRegex]"," attribute all use source generators to produce\nzero-overhead, AOT-compatible code. When you see ",[24,638,639],{},"[JsonSerializable(typeof(MyType))]","\nin modern .NET code, that is a source generator at work.",[10,642,644],{"id":643},"recap","Recap",[15,646,647,648,651,652,654,655,658,659,662,663,666],{},"The CLR is the execution engine for all .NET code. C# compiles to ",[49,649,650],{},"IL",", which the\nJIT compiler (RyuJIT) translates to native code on first call, caching the result\nfor the process lifetime. ",[49,653,134],{}," starts methods at tier 0 (fast JIT)\nand promotes hot methods to tier 1 (full optimisation) on a background thread.\nThe ",[49,656,657],{},"Common Type System"," ensures all .NET languages share the same type model,\nenabling cross-language interop. ",[49,660,661],{},"Assemblies"," are the unit of versioning and\ndeployment — they carry IL, metadata, and a manifest. The ",[49,664,665],{},"generational GC"," collects\nshort-lived objects cheaply in Gen 0 and reserves full-heap compaction for rare Gen 2\ncollections. Understanding these fundamentals is what lets you reason confidently about\nperformance, memory, and the difference between .NET versions.",[668,669,670],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":44,"searchDepth":81,"depth":81,"links":672},[673,674,675,676,680,681,682,687,688,689,690],{"id":12,"depth":81,"text":13},{"id":30,"depth":81,"text":31},{"id":55,"depth":81,"text":56},{"id":99,"depth":81,"text":100,"children":677},[678,679],{"id":133,"depth":497,"text":134},{"id":162,"depth":497,"text":163},{"id":186,"depth":81,"text":187},{"id":233,"depth":81,"text":234},{"id":296,"depth":81,"text":297,"children":683},[684,685,686],{"id":327,"depth":497,"text":328},{"id":397,"depth":497,"text":398},{"id":415,"depth":497,"text":416},{"id":430,"depth":81,"text":431},{"id":503,"depth":81,"text":504},{"id":611,"depth":81,"text":612},{"id":643,"depth":81,"text":644},"How the .NET CLR takes C# from source to running code — IL, tiered JIT compilation, garbage collection generations, and what changed between .NET Framework, .NET Core, and modern .NET.","medium","md","dotnet",{},true,"\u002Fblog\u002Fdotnet-clr-runtime-jit-managed-code","\u002Fdotnet\u002Ffundamentals\u002Fclr-runtime",{"title":5,"description":691},"blog\u002Fdotnet-clr-runtime-jit-managed-code","CLR Runtime","Fundamentals","fundamentals","2026-06-22","FGI-MguncKYmH9_fQhYEBtR09MGwLf9kZz-rGdJKGcw",1782244087172]