.NET Memory management is handled by the garbage collector, which automatically calls
finalizers and releases resources and clears memory within a .NET application. The garbage collector collects objects that are no longer being used by the application.
EditWhen Garbage Collection Starts
- Generation 0 is full. (In .NET 2.0 this occurs when Generation 0 contains around 256KB of memory).
- GC.Collect is called.
- Windows reports low memory.
- AppDomain unloading.
- CLR shutting down.
In all these situations, after suspending all managed threads, the collection will begin.
EditThe Algorithm
Every application has a set of roots, which is basically a set of pointers to the objects on the managed heap. These roots are added as references to objects are held within methods, and they are released when the references go out of scope. When the garbage collector starts, it assumes that all the objects within the thread are ready for garbage collection, and have no root. Starting with the method on the bottom of the call stack (the one currently running) the garbage collector marks every object in the method for which there is a root, by setting a flag in the object's sync block. In addition to this, any object that is referenced by the object that is marked is also marked. Objects that are already marked when the GC finds the object are not recursively searched, so as to prevent infinite loops. Next, the GC moves up the call stack one frame at a time and performs the same check in each method all the way back to the top of the call stack.
Next, the garbage collector goes through and looks for unmarked objects. These objects are what are known as garbage. At some point, these objects will be collected, and the memory that held these objects will be reclaimed.
Finally, the GC will look for large blocks of unclaimed contiguous memory. If it finds these blocks, it will compact the memory within the application by copying objects to new locations in memory, in order to try to maintain a relatively compacted heap.
EditMonitoring and Controlling Collection
Objects can be monitored and controlled as it pertains to garbage collection, by use of the GC handle table, accessed through the
GCHandle struct. To monitor or control an object, pass a reference to the object into the GCHandle's static Alloc method, along with a
GCHandleType enumeration flag indicating at what level of control you want to control the object.
The four levels of control with the GCHandleType enumeration are as follows:
- Weak - Indicating you can monitor when the GC has determined that the object is unreachable from code.
- WeakTrackResurrection - This differs from Weak in that the GCHandle is not cleared out until the finalizer has run.
- Normal - Allows you to control the lifetime of the object by forcing the GC to not collect the object even though there may not be any roots to the object.
- Pinned - Same as normal, except it also disallows compacting of the object when the GC compacts memory.
Normal and Pinned objects will be marked in addition to all the reachable objects that the Garbage Collector encounters during collection. Weak objects will be removed from the table at collection time if the object is unmarked, and if finalization is to take place, will be placed in the Freachable queue, at which point the object is marked, because it is considered reachable. Next, the WeakTrackResurrection items have their pointers set to null within the GC Handle Table. Finally, all the objects with the exception of the Pinned objects are compacted within the heap.
The Normal flag is typically passed in when referring to objects that will be used in conjunction with unmanaged memory. In scenarios where the unmanaged code requires the ability to work with the referenced object, the Pinned flag should be passed in. When the object is pinned, you would use the result of the
AddrOfPinnedObject method, which returns an IntPtr to send into the unmanaged code. This would contain the actual address of the object on the heap.
The Weak flag would to prevent one object from keeping another object from being collected. To do this, an object that would typically hold a reference to another object would not hold a reference to the other object, and would instead hold a GCHandle, and would use the GCHandle's
Target property to retrieve the object from the heap, and after using the object, would not maintain a reference to the object. Using this technique, the object referenced could then be collected, and would not be found as it has no root in the object using it.
The
WeakReference class is a wrapper around the GCHandle class.
To free the GCHandle from the table, Call the
Free method of the GCHandle.
EditGenerations
The .NET GC is known as a
generational garbage collector.
Every object is said to be in a
generation as concerns the garbage collector. The each generation has a budget of a specific amount of memory. Generations are numbered, and immediately after instantiation, all objects are placed in generation 0 (with the exception of objects that exceed 85,000 bytes, considered "large objects", which are placed in generation 2). After garbage collection, objects that are marked by the GC and survive a collection are moved up to the next generation, thus, objects that have survived garbage collection once are said to be in generation 1. Each generation is only collected when the maximum space allocated for that generation has been reached. Thus, imagine the following scenario:
Generation 0 has a limit of 256KB.
Generation 1 has a limit of 2MB.
1. Seven objects, each 32KB in size are allocated. They are placed in generation 0. The size of generation 0 is thus 224KB.
2. Six of these objects keep roots, but one loses it's root, and is no longer referenced.
3. Another object of 32KB is allocated, it is placed in generation 0. A reference is held to this object. The total size of generation 0 is now 256KB, so garbage collection takes place. When this collection takes place, 7 objects are found to have kept roots. These are placed in Generation 1. Generation 1 size is now 224KB, and generation 0 size is now 0KB.
4. Eight new objects are instantiated, each 32KB. They are all placed in Generation 0. With the eight one being instantiated, collection starts. All eight have roots, and are all placed in generation 1. Generation 1's size is now 480KB, far below the 2048KB limit in generation 1. The objects in generation 1 are not even checked. Generation 0 is now 0KB again.
This process continues until the memory in generation 1 reaches 2048 KB, at which point the objects in generation 1 will be cleared from memory if no root is found, those with roots will be moved to generation 2. Generation 2's limit is around 10MB in the .NET 2.0 runtime, and is used for objects that have survived two garbage collections, once in generation 0 and once in generation 1. Thresholds for each generation may be adjusted and may change over time to accommodate the situation in the particular application.
For example, if an application holds a reference to every object it creates in memory, the threshold for generation 2 might continue moving up infinitely. As objects survive two preceding collections, they would be added to generation 2, and if no objects in generation 2 are found to be without root, the threshold for generation 2 would simply have to increase, so garbage collection would be triggered again when more objects reach generation 2.
This
generational algorithm improves the performance of the GC, as
all objects are not collected immediately. The assumption is that newly constructed objects will have shorter lifetimes, while older objects will have longer lifetimes.
EditLimiting the Number of Open Handles for a Specific Type of Handle
To limit the number of handles that can be instantiated for a specific handle type, the Handle type can instantiate a
HandleCollector instance that will force collection of a specific type of object whenever more than one handle exists. To use the class, place a static HandleCollector within a class. Call the .Add method on object construction, and the .Remove() method in the finalizer. When the value within the handle collector exceeds the specified threshold for the class, a garbage collection will take place.
EditAddMemoryPressure and RemoveMemoryPressure
Occasionally a handle wraps an unmanaged object that can take a large amount of memory, while the managed object takes a minimal amount of memory. To handle this situation, call AddMemoryPressure and pass in the approximate number of bytes that you expect each object in unmanaged memory to take. Doing so will cause a higher frequency of garbage collections, to ensure that unmanaged objects taking a large amount of memory are collected earlier than normal. Be sure to call RemoveMemoryPressure when completed, to return the collection frequency to normal.
EditOther Programmatic Ways to Manage the Garbage Collector
Of course, the
Collect method is probably the most well-known way to force a collection, but using it is inadvisable except in very particular situations. Directly calling the garbage collector is
discouraged.
WaitForPendingFinalizers waits until the Freachable queue has been cleared.
GetGeneration will return the specific generation that an object currently resides in. This method takes either an object or a WeakReference.
GetTotalMemory returns the total number of bytes currently allocated on the heap.
CollectionCount returns the total number of collections that have occurred for the specified generation.