The .NET Framework uses automatic garbage collection to manage memory for all applications. When you use the new operator to create an object, the object’s memory is obtained from the managed heap. When the garbage collector decides that sufficient garbage has accumulated that it is efficient to do so, it performs a collection to free some memory. This process is fully automatic, but there are a number
of factors that you need to be aware of that can make the process more or less efficient.
of factors that you need to be aware of that can make the process more or less efficient.
To understand the principles of garbage collection, you need to understand the life cycle of a managed object
- Memory for an object is allocated from the managed heap when you call new. The object’s constructor is called after the memory is allocated.
- The object is used for a period of time.
- The object dies due to all its references either being explicitly set to null or else going out of scope.
- The object’s memory is freed (collected) some time later. After the memory is freed, it is generally available for other objects to use again
Allocation
The managed heap can be thought of as a block of contiguous memory. When you create a new object, the object’s memory is allocated at the next available location on the managed heap. Because the garbage collector does not need to search for space, allocations are extremely fast if there is enough memory available. If there is not enough memory for the new object, the garbage collector attempts to reclaim
space for the new object.
Collection
To reclaim space, the garbage collector collects objects that are no longer reachable. An object is no longer reachable when there are no references to it, all references are set to null, or all references to it are from other objects that can be collected as part of the current collection cycle.
When a collection occurs, the reachable objects are traced and marked as the trace proceeds. The garbage collector reclaims space by moving reachable objects into the contiguous space and reclaiming the memory used by the unreachable objects. Any object that survives the collection is promoted to the next generation.
Generations
The garbage collector uses three generations to group objects by their lifetime and volatility:
- Generation 0 (Gen 0). This consists of newly created objects. Gen 0 is collected frequently to ensure that short-lived objects are quickly cleaned up. Those objects that survive a Gen 0 collection are promoted to Generation 1.
- Generation 1 (Gen 1). This is collected less frequently than Gen 0 and contains longer-lived objects that were promoted from Gen 0.
- Generation 2 (Gen 2). This contains objects promoted from Gen 1 (which means it contains the longest-lived objects) and is collected even less frequently. The general strategy of the garbage collector is to collect and move longer-lived objects less frequently.
Key GC Methods
Below table shows the key methods of the System.GC class. You can use this class to control the behavior of the garbage collector.
Server GC vs. Workstation GC
The CLR provides two separate garbage collectors:
Workstation GC (Mscorwks.dll). This is designed for use by desktop applications such as Windows Forms applications.
Server GC (Mscorsvr.dll). This is designed for use by server applications. ASP.NET loads server GC but only if the server has more than one processor. On single processor servers, it loads workstation GC.
Server GC is optimized for throughput, memory consumption, and multiprocessor scalability, while the workstation GC is tuned for desktop applications. When using the server GC, the managed heap is split into several sections, one per CPU on a multiprocessor computer. When a collection is initiated, the collector has one thread per CPU; all threads collect their own sections simultaneously. The workstation version of the execution engine (Mscorwks.dll) is optimized for smaller latency. Workstation GC performs collection in parallel with the CLR threads. Server GC suspends the CLR threads during collection.
You might sometimes need the functionality of the server GC for your custom application when hosting it on a multiprocessor computer. For example, you might need it for a Windows service that uses a .NET remoting host and is deployed on a multiprocessor server. In this scenario, you need to develop a custom host that loads the CLR and the server GC version of the garbage collector.
Identify and Analyze Your Application’s Allocation Profile
Object size, number of objects, and object lifetime are all factors that impact your application’s allocation profile. While allocations are quick, the efficiency of garbage collection depends (among other things) on the generation being collected. Collecting small objects from Gen 0 is the most efficient form of garbage collection because Gen 0 is the smallest and typically fits in the CPU cache. In contrast, frequent collection of
objects from Gen 2 is expensive. To identify when allocations occur, and which generations they occur in, observe your application’s allocation patterns by using an allocation profiler such as the CLR Profiler.
For more information, see “How To: Use CLR Profiler” in the “How To” section of this guide.
Avoid Calling GC.Collect
The default GC.Collect method causes a full collection of all generations. Full collections are expensive because literally every live object in the system must be visited to ensure complete collection. Needless to say, exhaustively visiting all live objects could, and usually does, take a significant amount of time. The garbage collector’s algorithm is tuned so that it does full collections only when it is likely to be worth the expense of doing so. As a result, do not call GC.Collect directly — let the garbage collector determine when it needs to run.
The garbage collector is designed to be self-tuning and it adjusts its operation to meet the needs of your application based on memory pressure. Programmatically forcing collection can hinder tuning and operation of the garbage collector.
If you have a particular niche scenario where you have to call GC.Collect, consider the following:
Call GC.WaitForPendingFinalizers after you call GC.Collect. This ensures that the current thread waits until finalizers for all objects are called.
After the finalizers run, there are more dead objects (those that were just finalized) that need to be collected. One more call to GC.Collect collects the remaining dead objects.
System.GC.Collect(); // This gets rid of the dead objects
System.GC.WaitForPendingFinalizers(); // This waits for any finalizers to finish.
System.GC.Collect(); // This releases the memory associated with the objects that were just finalized.
Consider Using Weak References with Cached Data
Consider using weak references when you work with cached data, so that cached objects can be resurrected easily if needed or released by garbage collection when there is memory pressure. You should use weak references mostly for objects that are not small in size because the weak referencing itself involves some overhead. They are suitable for medium to large-sized objects stored in a collection.
Consider a scenario where you maintain a custom caching solution for the employee information in your application. By holding onto your object through a WeakReference wrapper, the objects are collected when memory pressure grows during periods of high stress. If on a subsequent cache lookup, you cannot find the object, re-create it from the information stored in an authoritative persistent source. In this way, you balance the use of cache and persistent medium. The following code demonstrates how to use a weak reference.
void SomeMethod()
{
// Create a collection
ArrayList arr = new ArrayList(5);
// Create a custom object
MyObject mo = new MyObject();
// Create a WeakReference object from the custom object
WeakReference wk = new WeakReference(mo);
// Add the WeakReference object to the collection
arr.Add(wk);
// Retrieve the weak reference
WeakReference weakReference = (WeakReference)arr[0];
MyObject mob = null;
if( weakReference.IsAlive ){
mob = (MyOBject)weakReference.Target;
}
if(mob==null){
// Resurrect the object as it has been garbage collected
}
//continue because we have the object
}
Prevent the Promotion of Short-Lived Objects
Objects that are allocated and collected before leaving Gen 0 are referred as short-lived objects. The following principles help ensure that your short-lived objects are not promoted:
● Do not reference short-lived objects from long-lived objects. A common example where this occurs is when you assign a local object to a class level object reference.
class Customer{
Order _lastOrder;
void insertOrder (int ID, int quantity, double amount, int productID){
Order currentOrder = new Order(ID, quantity, amount, productID);
currentOrder.Insert();
this._lastOrder = currentOrder;
}
}
Avoid this type of code because it increases the likelihood of the object being promoted beyond Gen 0, which delays the object’s resources from being reclaimed. One possible implementation that avoids this issue follows.
class Customer{
int _lastOrderID;
void ProcessOrder (int ID, int quantity, double amount, int productID){
. . .
this._lastOrderID = ID;
. . .
}
}
The specific Order class is brought in by ID as needed.
- Avoid implementing a Finalize method. The garbage collector must promote finalizable objects to older generations to facilitate finalization, which makes them long-lived objects.
- Avoid having finalizable objects refer to anything. This can cause the referenced object(s) to become long-lived.
Set Unneeded Member Variables to Null Before Making
Long-Running Calls
Before you block on a long-running call, you should explicitly set any unneeded member variables to null before making the call so they can be collected. This is demonstrated in the following code fragment.
class MyClass{
private string str1;
private string str2;
void DoSomeProcessing(...){
str1= GetResult(...);
str2= GetOtherResult(...);
}
void MakeDBCall(...){
PrepareForDBCall(str1,str2);
str1=null;
str2=null;
// Make a database (long running) call
}
}
This advice applies to any objects which are still statically or lexically reachable but are actually not needed:
- If you no longer need a static variable in your class, or some other class, set it to null.
- If you can “prune” your state, that is also a good idea. You might be able to eliminate most of a tree before a long-running call, for instance.
- If there are any objects that could be disposed before the long-running call, set those to null.
void func(...)
{
String str1;
str1="abc";
// Avoid this
str1=null;
}
Minimize Hidden Allocations
Memory allocation is extremely quick because it involves only a pointer relocation to create space for the new object. However, the memory has to be garbage collected at some point and that can hurt performance, so be aware of apparently simple lines of code that actually result in many allocations. For example, String.Split uses a delimiter to create an array of strings from a source string. In doing so, String.Split allocates a new string object for each string that it has split out, plus one object for the array. As a result, using String.Split in a heavy duty context (such as a sorting routine) can be expensive.string attendees = "bob,jane,fred,kelly,jim,ann";
// In the following single line the code allocates 6 substrings,
// outputs the attendees array, and the input separators array
string[] names = attendees.Split( new char[] {','});
Also watch out for allocations that occur inside a loop such as string concatenations using the += operator. Finally, hashing methods and comparison methods are particularly bad places to put allocations because they are often called repeatedly in the context of searching and sorting. For more information about how to handle
strings efficiently.
Avoid or Minimize Complex Object Graphs
Try to avoid using complex data structures or objects that contain a lot of references to other objects. These can be expensive to allocate and create additional work for the garbage collector. Simpler graphs have superior locality and less code is needed to maintain them. A common mistake is to make the graphs too general.Avoid Preallocating and Chunking Memory
C++ programmers often allocate a large block of memory (using malloc) and then use chunks at a time, to save multiple calls to malloc. This is not advisable for managed code for several reasons:- Allocation of managed memory is a quick operation and the garbage collector has been optimized for extremely fast allocations. The main reason for preallocating memory in unmanaged code is to speed up the allocation process. This is not an issue for managed code.
- If you preallocate memory, you cause more allocations than needed; this can trigger unnecessary garbage collections.
- The garbage collector is unable to reclaim the memory that you manually recycle.
- Preallocated memory ages and costs more to recycle when it is ultimately released.
Finalize and Dispose Explained
The garbage collector offers an additional, optional service called finalization. Use finalization for objects that need to perform cleanup processing during the collection process and just before the object’s memory is reclaimed. Finalization is most often used to release unmanaged resources maintained by an object; any other use should be closely examined. Examples of unmanaged resources include file handles, database connections, and COM object references.
Finalize
Some objects require additional cleanup because they use unmanaged resources, and these need to be released. This is handled by finalization. An object registers for finalization by overriding the Object.Finalize method. In C# and Managed Extensions for C++, implement Finalize by providing a method that looks like a
C++ destructor.
Note: The semantics of the Finalize method and a C++ destructor should not be confused. The syntax is the same but the similarity ends there.
An object’s Finalize method is called before the object’s managed memory is reclaimed. This allows you to release any unmanaged resources that are maintained by the object. If you implement Finalize, you cannot control when this method should be called because this is left to the garbage collector — this is commonly
referred to as nondeterministic finalization. The finalization process requires a minimum of two collection cycles to fully release the object’s memory. During the first collection pass, the object is marked for finalization. Finalization runs on a specialized thread that is independent from the garbage collector. After finalization occurs, the garbage collector can reclaim the object’s memory. Because of the nondeterministic nature of finalization, there is no guarantee regarding the time or order of object collection. Also, memory resources may be consumed for a large amount of time before being garbage collected. In C#, implement Finalize by using destructor syntax.
class yourObject {
// This is a finalizer implementation
~yourObject() {
// Release your unmanaged resources here
. . .
}
}
The preceding syntax causes the compiler to generate the following code.
class yourObject {
protected override void Finalize() {
try{
. . .
}
finally {
base.Finalize();
}
}
Dispose
Provide the Dispose method (using the Dispose pattern, which is discussed later in this chapter) for types that contain references to external resources that need to be explicitly freed by the calling code. You can avoid finalization by implementing the IDisposable interface and by allowing your class’s consumers to call Dispose.
The reason you want to avoid finalization is because it is performed asynchronously and unmanaged resources might not be freed in a timely fashion. This is especially important for large and expensive unmanaged resources such as bitmaps or database connections. In these cases, the classic style of explicitly releasing your resources is preferred (using the IDisposable interface and providing a Dispose method). With
this approach, resources are reclaimed as soon as the consumer calls Dispose and the object need not be queued for finalization. Statistically, what you want to see is that almost all of your finalizable objects are being disposed and not finalized. The finalizer should only be your backup.
With this approach, you release unmanaged resources in the IDisposable.Dispose method. This method can be called explicitly by your class’s consumers or implicitly by using the C# using statement.
To prevent the garbage collector from requesting finalization, your Dispose implementation should call GC.SuppressFinalization.
Finalize and Dispose Guidelines
- Call Close or Dispose on classes that support it.
- Use the using statement in C# and Try/Finally blocks in Visual Basic .NET to ensure Dispose is called.
- Do not implement Finalize unless required.
- Implement Finalize only if you hold unmanaged resources across client calls.
- Move the Finalization burden to the leaves of object graphs.
- If you implement Finalize, implement IDisposable.
- If you implement Finalize and Dispose, use the Dispose pattern.
- Suppress finalization in your Dispose method.
- Allow Dispose to be called multiple times.
- Call Dispose on base classes and on IDisposable members.
- Keep finalizer code simple to prevent blocking.
- Provide thread safe cleanup code only if your type is thread safe.
Comments
Post a Comment