<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mehedi Hasan Shuvo</title>
    <description>The latest articles on DEV Community by Mehedi Hasan Shuvo (@mehedi_shuvo).</description>
    <link>https://dev.to/mehedi_shuvo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3811351%2F07d2aa94-9d95-4aee-bb7e-6609dcdd26b9.jpg</url>
      <title>DEV Community: Mehedi Hasan Shuvo</title>
      <link>https://dev.to/mehedi_shuvo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mehedi_shuvo"/>
    <language>en</language>
    <item>
      <title>Making .NET GC Behavior Observable: What I Learned Building GCExperiment</title>
      <dc:creator>Mehedi Hasan Shuvo</dc:creator>
      <pubDate>Sat, 07 Mar 2026 09:57:37 +0000</pubDate>
      <link>https://dev.to/mehedi_shuvo/making-net-gc-behavior-observable-what-i-learned-building-gcexperiment-h02</link>
      <guid>https://dev.to/mehedi_shuvo/making-net-gc-behavior-observable-what-i-learned-building-gcexperiment-h02</guid>
      <description>&lt;p&gt;I've been studying .NET GC internals and wanted to go beyond reading docs — so I built a small experiment suite to make the behavior actually visible.&lt;/p&gt;

&lt;p&gt;This post walks through what I learned and what each experiment demonstrates. The full repo is at the end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why experiments?
&lt;/h2&gt;

&lt;p&gt;Documentation tells you &lt;em&gt;what&lt;/em&gt; the GC does. Running code tells you &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt;. The difference matters when you're trying to reason about allocation pressure in real systems.&lt;/p&gt;

&lt;p&gt;The project is called &lt;strong&gt;GCExperiment&lt;/strong&gt; — four isolated experiments, each targeting one GC mechanic, all instrumented with real snapshots.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Built on .NET 10 / C# 14, x64 only. Header sizes and alignment differ on x86, so 64-bit is required for realistic LOH behavior.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Experiment 1 — LOH Placement
&lt;/h2&gt;

&lt;p&gt;The most surprising thing I learned: &lt;strong&gt;it's not a simple 85,000-byte threshold.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The GC measures the &lt;em&gt;full object cost&lt;/em&gt; — header, payload, and alignment. Here's the actual math for &lt;code&gt;byte[84_999]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;24 bytes  (array header)
+ 84,999  (payload)
= 85,023  → aligned to 85,024 → LOH threshold hit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;byte[84_999]&lt;/code&gt; goes to the Large Object Heap — not because the payload exceeds 85K, but because the aligned full object does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the experiment shows:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where arrays of different sizes actually land (SOH vs LOH)&lt;/li&gt;
&lt;li&gt;How to measure full object size with &lt;code&gt;GCInfo.EstimateSizeByFactory&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How to force LOH compaction when needed:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;GCSettings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LargeObjectHeapCompactionMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCLargeObjectHeapCompactionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompactOnce&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GCCollectionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Forced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Experiment 2 — Generation Promotion
&lt;/h2&gt;

&lt;p&gt;Objects don't live forever in Gen0. Survivors get promoted — and this experiment makes that visible in real time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;GC.GetGeneration(obj)&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recently allocated, ephemeral&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Survived one Gen0 collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long-lived, or on the LOH (LOH objects start at Gen2 immediately)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing I didn't expect: &lt;code&gt;GC.KeepAlive&lt;/code&gt; genuinely changes outcomes when you're observing promotion. Without it, the JIT can optimize away your reference and the object gets collected before you can inspect its generation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Experiment 3 — Allocation Pressure
&lt;/h2&gt;

&lt;p&gt;This one compares two patterns side by side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived small objects&lt;/strong&gt; — allocate, discard, repeat → hammers Gen0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-lived large objects&lt;/strong&gt; → builds Gen2/LOH pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The output prints live collection counts as the experiment runs. Changing sample sizes and watching how Gen0 vs Gen2 counts diverge is genuinely useful for building intuition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Experiment 4 — LOH Fragmentation
&lt;/h2&gt;

&lt;p&gt;This one was the most eye-opening.&lt;/p&gt;

&lt;p&gt;If you alternate large allocations — allocate A, allocate B, free A, try to allocate C — the hole left by A may not fit C. The LOH is not compacted by default, so those holes accumulate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[  A  ][ B ][ hole ][ B ][ hole ]
                ↑
         C doesn't fit here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why LOH compaction and buffer pooling (&lt;code&gt;ArrayPool&amp;lt;T&amp;gt;&lt;/code&gt;) exist. The experiment makes the fragmentation visible through allocation failures and GC snapshot comparisons.&lt;/p&gt;




&lt;h2&gt;
  
  
  Diagnostics &amp;amp; Helpers
&lt;/h2&gt;

&lt;p&gt;The repo includes two instrumentation helpers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;GCMonitor&lt;/code&gt;&lt;/strong&gt; (in &lt;code&gt;GCMastery.Diagnostics&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;GCMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrintSnapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"after allocation"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;GCMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrintObjectGeneration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myObj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myObj&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;GCMonitor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clean baseline"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;GCInfo&lt;/code&gt;&lt;/strong&gt; (in &lt;code&gt;GCExperiment&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCurrentState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forceFullCollectionFirst&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;gen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetGeneration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myObj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EstimateSizeByFactory&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;84_999&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sampleCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Running Everything
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;GenerationExperiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;LOHExperiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;FragmentationExperiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;PressureExperiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One tip before measuring anything — always start clean:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GCCollectionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Forced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForPendingFinalizers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;GC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GCCollectionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Forced&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;blocking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise leftover state from previous runs pollutes your results.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm Still Figuring Out
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Server GC vs Workstation GC behavior differences&lt;/li&gt;
&lt;li&gt;How &lt;code&gt;GCSettings.LatencyMode&lt;/code&gt; affects promotion timing&lt;/li&gt;
&lt;li&gt;Better ways to visualize fragmentation over time (currently console-only)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've worked with any of this and spot something off — or have ideas for experiments worth adding — I'd love the feedback in the comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/Shuvo091/GCExperiment" rel="noopener noreferrer"&gt;https://github.com/Shuvo091/GCExperiment&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;.NET 10 · C# 14 · x64 required&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>github</category>
      <category>dotnet</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
