Allocation
A C# program keeps data in two places. Locals live in the method's stack frame. Objects made with new live on the managed heap, memory the CLR owns. A local like a holds only a reference: the object's address.
Line 1 runs new Order(1). The CLR puts the object at the next free address in generation 0 and moves the allocation pointer past it. That is nearly all allocation costs. Now a holds the address, 0xA000.
Line 2 places Order(2) right after the first, at 0xA040. Every new small object starts in generation 0, the youngest part of the heap. Most objects die young, so gen 0 is where the GC collects most often.
Line 3 sets b to null. Now nothing refers to Order(2): no local, no field, no other object. It is unreachable, so it is garbage. Its memory is not freed yet; that waits for the next collection.
A gen 0 collection
A collection starts when allocations pass a threshold the runtime keeps tuning, when the machine runs low on memory, or when code calls GC.Collect. Line 4 forces a gen 0 collection, for this demo only. First the GC suspends the other managed threads.
The GC starts from the roots: locals on each thread's stack, static fields, CPU registers and GC handles. It follows every reference and marks each object it reaches. a leads to Order(1). Nothing leads to Order(2), so it stays unmarked.
Every unmarked object is dead. Order(2) has no finalizer, so the GC simply reclaims its memory: you never call free or delete. Space that held garbage is ready to be used again.
Survivors move up a generation: Order(1) is packed into gen 1 at 0x8000, and the GC updates a to that new address. Gen 0 is empty again, so the next new is once more a simple pointer bump.
Generations
The collection is over and the program carries on. Line 5 asks GC.GetGeneration(a): the object survived one collection, so it returns 1. Line 6 prints 1. If it survived a gen 1 collection too, it would move to gen 2.
Gen 0 collections are frequent and quick; a gen 2 collection is a full one, rare and costly. An object of 85,000 bytes or more, like new byte[85000], skips gen 0: it goes to the large object heap, collected with gen 2.
What happens
// step 1/10 · two-places
stack frame Main's locals: a, b, g
gone when the method returns
managed heap every object made with new
the CLR owns it; the garbage collector (GC) frees it
a reference the address of an object on the heap
The addresses (0xA000, 0xA040, 0x8000) are made up and shortened, and the gaps between them are not to scale. The picture shows the classic layout: survivors slide into gen 1. Since .NET 7 the heap is organised in regions, and a whole gen 0 region can also simply become gen 1; the GC may also sweep without compacting. Either way the object survives and its generation becomes 1.