Garbage Collection

Garbage collection (GC) is a memory recovery feature built into the framework. It runs in its own thread from your application.

Also see Value Type vs Reference Type

Finalizer

A method that is call at the moment that an object is being garbage collected. Ive never had a use case for this besides demo code like this to keep track and show that GC is happening.

Here ~Foo is the finalizer.

1
2
3
4
5
6
7
8
Public class Foo
{
public string Prop { get; set; }
~Foo()
{
Console.WriteLine($"GC: {Name}")
}
}

You can instruct GC to cleanup but this is not something Ive ever needed to manually do, instead I trust that the GC Engine knows the suitable time to take out the trash. Normally when the runtime system is starting to run short of memory :)

Example instance code

1
2
3
4
5
6
7
static void Main()
{
var fooInstance = new Foo() { Prop = "I was cleaned up" };

GC.Collect();
GC.WaitForPendingFinalizers();
}

This wont output anything.

The line GC.Collect() will look for all of the local stack and static variables in our entire application. Anything referenced on the heap, from the stack variable that is reachable is marked as reachable. So that wont be garbage collected as its in scope when we called GC.Collect()

So if we change the code as follows

1
2
3
4
5
6
7
8
9
10
11
static void Main()
{
CreateFoo();
GC.Collect();
GC.WaitForPendingFinalizers();
}

static void CreateFoo()
{
var fooInstance = new Foo() { Prop = "I was cleaned up" };
}

This will output: GC: I was cleaned up as fooInstance is out of scope when we run GC so all memory will be re-claimed.

Generations

In .Net there are 3 heaps, its called a 3 generational garbage collection system. Note that there is also a large object heap (anything greater than 85 kilobytes)

  • Generation 0 (short lived objects where GC runs more often)

Initially this is where a reference object is allocated. It stores the objects like a stack ontop of each other and has a heap pointer for the next avalible space. When marking happens, anything that is reachable is moved to Generation 1. So now the pointer is moved to 0 and new objects coming in simply replace the existing ones and the stack start again. This means there is no heap fragmentation. Additionally this gives a performance gain.

  • Generation 1 (collected less frequently than Gen 0)

These are objects that were marked as reachable and proved to have a reasonably long lifetime. When Generation 1 starts to get full the exact same process as above is performed and anything marked as reachable moves to Generation 2

  • Generation 2 (collected less frequently than Gen 1)

These objects have a really long lifetime and are likely to be around for the entire lifetime of the application. When Generation 2 starts to get full, the framework will start to compact the heap. This just means that unreachable objects are removed, this is a slower process but should not happen much if at all. (thats NOT a challange to write bad code!)

  • Large Object Heap (collected less frequently)

These are for large objects (anything over 85 kilobytes) and are assumed to be long lived based on their size. Large Object Heap behaves on its own and is like the Generation 2 heap so it will instead be compacted when GC happens.

These large objects will generally be things like Lists/arrays.

References