Click here to Skip to main content
15,881,898 members
Articles / Programming Languages / C#

A Super-Simplified Explanation of .NET Garbage Collection

Rate me:
Please Sign up or sign in to vote.
4.87/5 (50 votes)
19 Aug 2018CPOL6 min read 33.2K   70   9
This article is super-simplified, look at .NET garbage collection, with loads of intentional technical omissions. It aims to provide a baseline level of understanding that a typical C# developer realistically needs for their day-to-day work.

Garbage collection is so often at the root (excuse the pun) of many performance problems, very often because of misunderstanding, so please do set aside time to deepen your understanding after reading this.

This article is super-simplified, look at .NET garbage collection, with loads of intentional technical omissions. It aims to provide a baseline level of understanding that a typical C# developer realistically needs for their day-to-day work. I certainly won’t add complexity by mentioning the stack, value types, boxing, etc.

Disclaimer: The .NET garbage collector is a dynamic beast that adapts to your application and its implementation is often changed.

What Developers Know To Say At Interview

The garbage collector automatically looks for objects that are no longer used and frees up that memory. This helps avoid memory leaks created through programmer error.

That’s fine to get the job but is not enough to engineer good, performant C#, trust me.

Managed Memory (The Brief Version)

The .NET CLR (Common Language Runtime) reserves a chunk of memory available to your application where it will manage any objects allocated by your application. When your application is finished with these objects, they are deallocated. This part is handled by the Garbage Collector (GC).

The GC can and does expand segments sizes when needed but its preference is to reclaim space through its generational garbage collection…

The Generational .NET Garbage Collector

New, small, objects go into generation 0. When a collection occurs, any objects that are no longer in use (no references to them) have their memory freed up (deallocated). Any objects still in use will survive and be promoted to the next generation.

Image 1

Live Long Short (or Forever) and Prosper

Image 2

Ask any .NET expert and they will tell you the same thing – an object should be short-lived or else live forever. I won’t be going into details about performance – this is the one and only rule I want you to take away.

To appreciate it, we need to answer: Why generational?

In a well-engineered C# application, typical objects will live and die without ever being promoted out of gen 0. I’m thinking of operations like:

  • Variables local to a short running method
  • Objects instantiated for the lifetime of a request to a web API call

Gen 1 is the ‘generation in between’, which will catch any wannabe-short-lived objects that escape gen 0, with what is still a relatively quick collection.

A check on which objects are unused consumes resources and suspends applications threads. GC gets increasingly expensive up the generations, as a collection of a particular generation has to also collect all those preceding it, e.g., if gen 2 is collected, then so also must gen 1 and gen 0 (see the above diagram). This is why we often refer to gen 2 as the full GC. Also, objects that live longer tend to be more complicated to clean up!

Don’t worry though – if we know which objects are likely to live longer, we can just check them less frequently. And with this in mind:

.NET GC runs the most on gen 0, less on gen 1 and even less often on gen 2.

If an object makes it to gen 2, it needs to be for a good reason – like being a permanent, reusable object. If objects make it there unintentionally, they’ll stick around longer, using up memory and resulting in more of those bigger gen 2 full collections!

But Generations Are All Just A Front!

The biggest gotcha when exploring your application through profilers and debuggers, looking at GC for the first time, is the Large Object Heap (LOH) also being referred to as generation 2.

Physically, your objects will end up in managed heap segments (in the memory allocated to the CLR, mentioned earlier).

Image 3

Small objects will be added onto gen 0 of the Small Object Heap (SOH) consecutively, avoiding having to look for free space. To reduce fragmentation when objects die, the heap may be compacted.

See the following simplified look at a gen 0 collection, followed by a gen 1 collection, with a new object allocation in between (my first go at such an animation).

Click on the animation if you would like to open a video of the animation you can pause, etc.


Image 4


Large objects go on the Large Object Heap, do not automatically compact, but will try to reuse space:

Image 5

As of .NET451, you can tell the GC to compact it on the next collection. But prefer other options for dealing with LOH fragmentation such as pooling reusable objects instead.

How Big Is a Large Object?

It is well established that >= 85KB is a large object (or an array of 1000 doubles). Need to know a bit more than that…

You might be thinking of that large Bitmap image you’ve worked with – actually that object uses 24 Bytes and the bitmap itself is in unmanaged memory. It’s really rare to see an object that is really large. More typically, a large object is going to be an array.

In the following example, the object from LargeObjectHeapExample is actually 16 bytes because it is just made up of general class information and pointers to the string and byte array.

By instantiating the LargeObjectHeapExample object, we are actually allocating 3 objects on the heap: 2 of them on the small object heap; and the byte array on the large object heap.

Image 6

Remember what I said earlier about stuff in the Large Object Heap – notice how the byte array reports as being in generation 2! One reason for the LOH being within gen 2 logically, is that large objects typically have longer lifetimes (think back to what I said earlier about objects that live longer in generational GC). The other reason is the expense of copying large objects while performing the compacting that occurs in earlier generations.

What Triggers a Collection?

  • An attempt to allocate exceeds threshold for a generation or the large object heap
  • A call to GC.Collect (I’ll save this for another article)
  • The OS signals low system memory

Remember gen 2 and the LOH are logically the same thing, so hitting the threshold on either, will trigger a full (gen 2) collection on both heaps. Something to consider re performance (beyond this article).

Summary

  • A collection of a particular generation also collects all those below it, i.e., collecting 2 also collects 1 and 0.
  • The GC promotes objects that survive collection (because they are still in use) to the next generation. Although see the previous point – don’t expect an object in gen 1 to move to gen 2 when a gen 0 collection occurs.
  • GC runs the most on gen 0, less on gen 1 and even less often on gen 2. With this in mind, objects should be short-lived (die in gen 0 or gen 1 at worst) or live forever (intentionally of course) in gen 2.

History

  • 20/08/2018 - Fixed import formatting problem & added the array doubles threshold for completeness

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
Ben is the Principal Developer at a gov.uk and .NET Foundation foundation member. He previously worked for over 9 years as a school teacher, teaching programming and Computer Science. He enjoys making complex topics accessible and practical for busy developers.


Comments and Discussions

 
Questioninfographic used? Pin
prateek bhardwaj6024-May-23 18:48
prateek bhardwaj6024-May-23 18:48 
GeneralMy vote of 5 Pin
Ehsan Sajjad16-Oct-18 2:27
professionalEhsan Sajjad16-Oct-18 2:27 
nice elaboration using graphics
GeneralMy vote of 5 Pin
ThatsAlok15-Aug-18 20:40
ThatsAlok15-Aug-18 20:40 
Praisevery nice Pin
BillW3310-Aug-18 3:25
professionalBillW3310-Aug-18 3:25 
QuestionExecution Time Pin
George Swan16-Jul-18 18:20
mveGeorge Swan16-Jul-18 18:20 
AnswerRe: Execution Time Pin
Ben Hall (failingfast.io)17-Jul-18 8:41
professionalBen Hall (failingfast.io)17-Jul-18 8:41 
GeneralRe: Execution Time Pin
George Swan17-Jul-18 10:26
mveGeorge Swan17-Jul-18 10:26 
QuestionLow Memory Event Pin
Alois Kraus15-Jul-18 12:11
Alois Kraus15-Jul-18 12:11 
GeneralApologies for gif animation instead of video Pin
Ben Hall (failingfast.io)15-Jul-18 3:13
professionalBen Hall (failingfast.io)15-Jul-18 3:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.