Perhaps you missed it: Eric Lippert has mentioned it in a side note:
[...] and static variables are stored on the heap.
That's written in the context of
The truth is that this is an implementation detail [...]
Here's how the Microsoft implementation does it:
But why are static variables stored on the heap?
Well, even the Main() method does not live forever. The Main() method could end and some other threads could still be running. What should happen in such a case to the struct? It needn't necessarily be on the heap, but I hope you see that it can't be on the stack and not in a register. The struct must be somewhere for other threads to still be able to access it. Heaps are a good choice.
Code example where Main() dies:
using System;
using System.Threading;
public class Program
{
public static CustomStructType inst1;
static void Main(string[] args)
{
new Thread(AccessStatic).Start();
//assigning an instance of value type to the field
inst1 = new CustomStructType();
Console.WriteLine("Main is gone!");
}
static void AccessStatic()
{
Thread.Sleep(1000);
Console.WriteLine(inst1);
Console.ReadLine();
}
}
public struct CustomStructType
{
//body
}
Let's get back to your original code. When in doubt, you can always check with a debugger. This is a debug session of a Release build in .NET Framework 4.8 (4.8.4341.0).
I'm debugging with WinDbg Preview, which is a free debugger provided by Microsoft. It's not convenient to use, though. I learned about it from the book "Advanced .NET debugging" by Mario Hewardt.
I inserted a Console.ReadLine()
for simplicity, so I don't need to step through everything and stop at the right time.
Load the .NET extension
ntdll!DbgBreakPoint:
77534d10 cc int 3
0:006> .loadby sos clr
Search for an instance of Program
(just to check whether the premise of the question is correct) indeed gives 0 objects:
0:007> !dumpheap -type Program
Address MT Size
Statistics:
MT Count TotalSize Class Name
Total 0 objects
Search for the class:
0:006> !name2ee *!Program
Module: 787b1000
Assembly: mscorlib.dll
--------------------------------------
Module: 01724044
Assembly: StructOnHeap.exe
Token: 02000002
MethodTable: 01724dcc
EEClass: 01721298 <--- we need this
Name: Program
Get information about the class:
0:006> !dumpclass 01721298
Class Name: Program
mdToken: 02000002
File: C:\...\bin\Release\StructOnHeap.exe
Parent Class: 787b15c8
Module: 01724044
Method Table: 01724dcc
Vtable Slots: 4
Total Method Slots: 5
Class Attributes: 100001
Transparency: Critical
NumInstanceFields: 0
NumStaticFields: 1
MT Field Offset Type VT Attr Value Name
01724d88 4000001 4 CustomStructType 1 static 0431357c inst1
^-- now this
Check where the garbage collected heaps are:
0:006> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x03311018
generation 1 starts at 0x0331100c
generation 2 starts at 0x03311000
ephemeral segment allocation context: none
segment begin allocated size
03310000 03311000 03315ff4 0x4ff4(20468)
Large object heap starts at 0x04311000
segment begin allocated size
04310000 04311000 04315558 0x4558(17752) <-- look here
Total Size: Size: 0x954c (38220) bytes.
------------------------------
GC Heap Size: Size: 0x954c (38220) bytes.
Yes, it's on the large object heap which starts at 0x04311000.
BTW: It was astonishing to me that such a small "object" (struct) will be allocated on the large object heap. Typically, the LOH will contain objects with 85000+ bytes only. But it makes sense, because the LOH is typically not garbage collected and you don't need to garbage collect static
items.