{"id":54206,"date":"2025-11-26T07:17:27","date_gmt":"2025-11-26T07:17:27","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=54206"},"modified":"2026-02-21T08:29:13","modified_gmt":"2026-02-21T08:29:13","slug":"dotnet-understanding-garbage-collection-with-gen0-gen1-gen2","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/dotnet-understanding-garbage-collection-with-gen0-gen1-gen2\/","title":{"rendered":"DOTNET: Understanding Garbage Collection with Gen0, Gen1, Gen2"},"content":{"rendered":"\n<p>Yes, we can absolutely do this in <strong>one self-contained program<\/strong> \ud83d\udc4d<br>You\u2019ll be able to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>See <strong>GC before\/after<\/strong> each scenario<\/li>\n\n\n\n<li>Trigger <strong>Gen0 \/ Gen1 \/ Gen2<\/strong> collections<\/li>\n\n\n\n<li>Observe <strong>heap size<\/strong> and <strong>collection counts<\/strong><\/li>\n\n\n\n<li>Use this with <strong>dotnet-counters \/ dotMemory \/ VS profiler<\/strong> if you want<\/li>\n<\/ul>\n\n\n\n<p>Below is a <strong>single <code>Program.cs<\/code><\/strong> you can drop into a new console app.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">1\ufe0f\u20e3 Create the project<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">dotnet <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">console<\/span> -n GcGenerationsDemo\ncd GcGenerationsDemo\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Replace <strong>all contents<\/strong> of <code>Program.cs<\/code> with this:<\/p>\n\n\n\n<script src=\"https:\/\/gist.github.com\/devops-school\/99d99d45e2aa02fc8b64427fe4ad4182.js\"><\/script>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">\n<\/code><\/span><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">2\ufe0f\u20e3 How to run and \u201cexperience\u201d GC<\/h2>\n\n\n\n<p>From inside the project folder:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">dotnet run\n<\/code><\/span><\/pre>\n\n\n<p>The program will walk you through:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Startup stats<\/strong><\/li>\n\n\n\n<li><strong>Gen0 scenario<\/strong> \u2013 short-lived allocations<\/li>\n\n\n\n<li><strong>Gen1 scenario<\/strong> \u2013 medium-lived allocations<\/li>\n\n\n\n<li><strong>Gen2 scenario<\/strong> \u2013 long-lived allocations<\/li>\n<\/ol>\n\n\n\n<p>Each step prints:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>Gen0\/Gen1\/Gen2 collection counts<\/code><\/li>\n\n\n\n<li><code>Total managed heap (MB)<\/code><\/li>\n<\/ul>\n\n\n\n<p>You\u2019ll see:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Gen0 counts jump a lot in the first scenario.<\/li>\n\n\n\n<li>Gen1 counts increase in the second scenario.<\/li>\n\n\n\n<li>Gen2 counts and heap size behavior change in the third scenario.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">3\ufe0f\u20e3 Optional: Watch it live with <code>dotnet-counters<\/code><\/h2>\n\n\n\n<p>In another terminal, while the app is running:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Get process list: <code>dotnet-counters ps<\/code><\/li>\n\n\n\n<li>Find your <code>GcGenerationsDemo<\/code> PID, then: <code>dotnet-counters monitor --process-id &lt;PID&gt; System.Runtime<\/code><\/li>\n<\/ol>\n\n\n\n<p>Watch:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>gc-heap-size<\/code><\/li>\n\n\n\n<li><code>gen-0-gc-count<\/code><\/li>\n\n\n\n<li><code>gen-1-gc-count<\/code><\/li>\n\n\n\n<li><code>gen-2-gc-count<\/code><\/li>\n<\/ul>\n\n\n\n<p>Run each scenario and you\u2019ll see the counters move in sync with console output.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">4\ufe0f\u20e3 How this maps to Gen0 \/ Gen1 \/ Gen2 concepts<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Gen0 scenario<\/strong>\n<ul class=\"wp-block-list\">\n<li>Many tiny, short-lived objects<\/li>\n\n\n\n<li>Mostly collected in Gen0<\/li>\n\n\n\n<li>You\u2019ll see Gen0 collections spike<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Gen1 scenario<\/strong>\n<ul class=\"wp-block-list\">\n<li>Objects kept alive briefly in a <code>List&lt;byte[]&gt;<\/code><\/li>\n\n\n\n<li>They survive at least one collection \u2192 promoted to Gen1<\/li>\n\n\n\n<li>We force Gen1 collections and then free references<\/li>\n\n\n\n<li>You see Gen1 counts increase, heap shrink<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Gen2 scenario<\/strong>\n<ul class=\"wp-block-list\">\n<li>Objects stored in a static list (<code>LongLivedHolder.Buffers<\/code>)<\/li>\n\n\n\n<li>They are long-lived; promoted to Gen2<\/li>\n\n\n\n<li>Even after Gen2 collection, many remain because references are still held<\/li>\n\n\n\n<li>This is how <strong>leaks and long-lived caches<\/strong> behave<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"766\" height=\"922\" src=\"https:\/\/www.devopsschool.com\/blog\/wp-content\/uploads\/2025\/11\/image-32.png\" alt=\"\" class=\"wp-image-54207\" srcset=\"https:\/\/www.devopsschool.com\/blog\/wp-content\/uploads\/2025\/11\/image-32.png 766w, https:\/\/www.devopsschool.com\/blog\/wp-content\/uploads\/2025\/11\/image-32-249x300.png 249w\" sizes=\"auto, (max-width: 766px) 100vw, 766px\" \/><\/figure>\n\n\n\n<p><br><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">1\ufe0f\u20e3 How to read the four numbers<\/h2>\n\n\n\n<p>Each <code>PrintGcStats<\/code> gives you:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Gen0 collections<\/strong> \u2013 how many times .NET cleaned <strong>short-lived objects<\/strong><\/li>\n\n\n\n<li><strong>Gen1 collections<\/strong> \u2013 how many times it cleaned objects that survived Gen0<\/li>\n\n\n\n<li><strong>Gen2 collections<\/strong> \u2013 how many times it cleaned <strong>long-lived objects<\/strong><\/li>\n\n\n\n<li><strong>Total managed heap<\/strong> \u2013 how much managed memory is still in use <em>after<\/em> GC (roughly)<\/li>\n<\/ul>\n\n\n\n<p>These counters are <strong>cumulative since process start<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">2\ufe0f\u20e3 Gen0 scenario \u2013 short-lived objects<\/h2>\n\n\n\n<p>You saw:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">=== Gen0 Scenario: Short-lived objects ===\n--- GC Stats: Before Gen0 work ---\n  Gen0 collections: 0\n  Gen1 collections: 0\n  Gen2 collections: 0\n  Total managed heap: 0 MB\n\nGen0 scenario completed in 35 ms\n--- GC Stats: After Gen0 forced collection ---\n  Gen0 collections: 71\n  Gen1 collections: 1\n  Gen2 collections: 0\n  Total managed heap: 0 MB\n<\/code><\/span><\/pre>\n\n\n<p><strong>What happened?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We allocated <strong>millions of tiny objects<\/strong> in a loop.<\/li>\n\n\n\n<li>They were <strong>not stored anywhere<\/strong>, so they died quickly.<\/li>\n\n\n\n<li>GC cleaned them mostly in <strong>Gen0<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Gen0 collections: <code>0 \u2192 71<\/code> \u2705<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>A few objects survived briefly \u2192 promoted to Gen1 once:\n<ul class=\"wp-block-list\">\n<li>Gen1 collections: <code>0 \u2192 1<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>After the forced collection:\n<ul class=\"wp-block-list\">\n<li><code>Total managed heap: 0 MB<\/code> (rounded) \u2192 means <strong>almost nothing left<\/strong>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>\ud83d\udc49 <strong>Interpretation:<\/strong><\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cLots of short-lived garbage \u2192 GC handled it cheaply in Gen0.<br>We generated a ton of allocations, but the memory was fully reclaimed. No leak, GC working as designed.\u201d<\/p>\n<\/blockquote>\n\n\n\n<p>This is typical of <strong>request-scoped allocations<\/strong> in APIs when they\u2019re well-behaved.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">3\ufe0f\u20e3 Gen1 scenario \u2013 medium-lived objects<\/h2>\n\n\n\n<p>You saw:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">=== Gen1 Scenario: Medium-lived objects ===\n--- GC Stats: Before Gen1 work ---\n  Gen0 collections: 71\n  Gen1 collections: 1\n  Gen2 collections: 0\n  Total managed heap: 0 MB\n\nGen1 scenario completed in 2613 ms\n--- GC Stats: After Gen1 forced collections ---\n  Gen0 collections: 627\n  Gen1 collections: 296\n  Gen2 collections: 33\n  Total managed heap: 3131 MB\n<\/code><\/span><\/pre>\n\n\n<p><strong>What did the code do here?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>It allocated <strong>a lot of 16 KB buffers<\/strong> and stored them in a <strong><code>List&lt;byte[]&gt; survivors<\/code><\/strong>.<\/li>\n\n\n\n<li>That local list stayed alive for a while \u2192 the buffers <strong>survived multiple Gen0 collections<\/strong>.<\/li>\n\n\n\n<li>That caused:\n<ul class=\"wp-block-list\">\n<li>Gen0 collections: <code>71 \u2192 627<\/code> (lots of allocations)<\/li>\n\n\n\n<li>Gen1 collections: <code>1 \u2192 296<\/code> (many promotions &amp; cleanups)<\/li>\n\n\n\n<li>Gen2 collections: <code>0 \u2192 33<\/code> (some long-lived promotions too)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p><strong>Why is heap so big here (3131 MB)?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Right after the scenario, before the runtime has fully compacted and reused memory, <code>GetTotalMemory<\/code> sees ~3 GB still reserved\/used.<\/li>\n\n\n\n<li>These objects were <em>just<\/em> cleared at the end of the scenario (we call <code>survivors.Clear()<\/code> and GC.Collect), but this snapshot is still showing that a lot of memory was in play.<\/li>\n<\/ul>\n\n\n\n<p>Then before the next scenario you saw:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">--- GC Stats: Before Gen2 work ---\n  ...\n  Total managed heap: 2 MB\n<\/code><\/span><\/pre>\n\n\n<p>So <strong>eventually the runtime fully reclaimed it<\/strong>, and the heap dropped back down.<\/p>\n\n\n\n<p>\ud83d\udc49 <strong>Interpretation:<\/strong><\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cHere we created objects that lived longer than Gen0 (in a list).<br>We see big jumps in <strong>Gen1 and Gen2 collections<\/strong> and temporary heap growth (~3 GB).<br>After clearing references and more GC, memory drops back to a few MB \u2192 no leak, just heavy temporary pressure.\u201d<\/p>\n<\/blockquote>\n\n\n\n<p>This demonstrates:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Promotion from Gen0 \u2192 Gen1 \u2192 Gen2<\/strong><\/li>\n\n\n\n<li><strong>Longer-lived objects = more expensive GC<\/strong><\/li>\n\n\n\n<li>Why you don\u2019t want to keep large collections alive longer than necessary.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">4\ufe0f\u20e3 Gen2 scenario \u2013 long-lived \/ leaked objects<\/h2>\n\n\n\n<p>You saw:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">=== Gen2 Scenario: Long-lived objects ===\n--- GC Stats: Before Gen2 work ---\n  Gen0 collections: 627\n  Gen1 collections: 296\n  Gen2 collections: 33\n  Total managed heap: 2 MB\n\nGen2 scenario completed in 184 ms\n--- GC Stats: After Gen2 forced collection ---\n  Gen0 collections: 693\n  Gen1 collections: 329\n  Gen2 collections: 34\n  Total managed heap: 392 MB\n\nLong-lived objects stored: 50000\nNote: Because we still keep references, these objects won't be freed.\n<\/code><\/span><\/pre>\n\n\n<p><strong>What did the code do here?<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Allocated <strong>50,000 \u00d7 8 KB<\/strong> buffers \u2248 ~400 MB.<\/li>\n\n\n\n<li>Stored them in <strong><code>LongLivedHolder.Buffers<\/code><\/strong>, which is <code>static<\/code>.<\/li>\n\n\n\n<li>We <strong>never clear<\/strong> that list \u2192 those objects are effectively <strong>long-lived<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p>Even after a <strong>full Gen2 collection<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Gen2 collections: <code>33 \u2192 34<\/code> (we forced a full GC)<\/li>\n\n\n\n<li>But heap only drops to:\n<ul class=\"wp-block-list\">\n<li><code>Total managed heap: 392 MB<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>And we still have:\n<ul class=\"wp-block-list\">\n<li><code>Long-lived objects stored: 50000<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>\ud83d\udc49 <strong>Interpretation:<\/strong><\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cThese are long-lived objects (or a leak).<br>Even after a full Gen2 GC, almost 400 MB remains because we\u2019re <strong>still holding references<\/strong> in a static list.<br>This is what a memory leak \/ long-lived cache looks like in production:<br>Gen2 collections happen, but memory never really goes down.\u201d<\/p>\n<\/blockquote>\n\n\n\n<p>This is the pattern you\u2019d see in:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Static caches that don\u2019t evict<\/li>\n\n\n\n<li>Static lists\/dicts that only grow<\/li>\n\n\n\n<li>Singletons holding on to big data<\/li>\n\n\n\n<li>Event handler leaks, etc.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\">\n\n\n\n<h2 class=\"wp-block-heading\">5\ufe0f\u20e3 How to explain (short version)<\/h2>\n\n\n\n<p><\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>\u201cIn the <strong>Gen0 scenario<\/strong>, we created millions of tiny, short-lived objects.<br>GC handled them mostly in Gen0 (71 collections), and after GC, memory is basically 0 MB.<br>This is healthy, short-lived garbage.<\/p>\n<\/blockquote>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>In the <strong>Gen1 scenario<\/strong>, we kept objects alive for a while in a list.<br>They survived Gen0, got promoted to Gen1 and some to Gen2.<br>We see big Gen1\/Gen2 collection counts and temporary heap growth to ~3 GB, but after clearing references, the heap returns to a small size.<br>This shows the cost of medium-lived objects.<\/p>\n<\/blockquote>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>In the <strong>Gen2 scenario<\/strong>, we stored 50k buffers in a static list.<br>Even after a full Gen2 collection, we still use ~392 MB.<br>Because the app still references these objects, the GC cannot free them.<br>This is exactly how long-lived objects and memory leaks behave in real .NET apps.\u201d<\/p>\n<\/blockquote>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Yes, we can absolutely do this in one self-contained program \ud83d\udc4dYou\u2019ll be able to: Below is a single Program.cs you can drop into a new console app. 1\ufe0f\u20e3 Create the&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[11138],"tags":[],"class_list":["post-54206","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/54206","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=54206"}],"version-history":[{"count":3,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/54206\/revisions"}],"predecessor-version":[{"id":59889,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/54206\/revisions\/59889"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=54206"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=54206"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=54206"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}