Automatic Memory Management and Unmanaged Resources in .NET
- When Is Memory Released?
- IDisposable and the Dispose Design Pattern
- The C# "using" Syntax
- Conclusion
- References
Automatic memory management, also known as garbage collection, is a much-touted feature of the .NET runtime. With garbage collection, the .NET runtime keeps track of the memory that a program allocates, and can release that memory when the program no longer needs it. The way the .NET garbage collector manages this bit of magic makes for some interesting reading.
TIP
If you're at all curious about how it works, a good place to find more information is Jeffrey Richter's two-part series of articles in the November and December 2000 issues of MSDN Magazine.
The biggest benefit to memory management, as far as a programmer is concerned, is that programmers no longer need to worry about releasing allocated memory. This is unquestionably a Good Thing because memory leaks are among the most common programming errors, and a leading cause of poor program performance and program crashes.
But memory is only one of many scarce resources, and therein lies the problem. Memory leaks are a subset of a more general type of programming errorresource leaksthat share the same cause: the programmer's failure to release an allocated resource. The garbage collector takes care of releasing memory, which is the most commonly forgotten resource, and will even call your object's finalizer before the memory is released, but the finalizer also has to release any file handles, database connections, window handles, or other scarce system resources that it allocates.
Relying on the finalizer to clean up your unmanaged resources, though, is a recipe for disaster. You know that the garbage collector will call your finalizer during the next garbage collection pass, but you don't know when that will happen.
When Is Memory Released?
When you're writing C++ code for Windows, object destructors are executed when you call dispose to deallocate the object. After the object's destructor completes, the memory that the object occupied is returned to the heap. The compiler generates code to call the destructors for stack-allocated objects when the objects go out of scope. This behavior is termed deterministic finalizationobject destructors are called at well-defined points in the program.
.NET has no analog to the C++ dispose operation. Once you allocate an object, it remains allocated until the garbage collector, on the next garbage collection pass, determines that the object is no longer being used. At that point, the garbage collector calls the object's finalizer (analogous to a C++ destructor) and then releases the object's memory back to the heap. This is called nondeterministic finalizationyou can't say when an object's finalizer will be called. The result is that any memory or unmanaged resources that your object uses remain allocated for an indeterminate time after the object itself is no longer actually in use by your program.
The garbage collector runs on its own schedule. As far as memory is concerned, this doesn't normally cause a problem because the runtime will force a garbage collection if it's unable to satisfy a memory allocation request. But a program that uses many files or makes many different database connections could find itself running out of resources because all of the file handles or connections are allocated by objects that are waiting for finalization. It's kind of like going to Blockbuster Video just after noon and seeing the hot new release behind the counter, but being unable to rent it because it hasn't been officially checked in.
A Simple Example
AllocExample, shown in Listing 1, illustrates this problem. This program performs a loop that allocates a CoolThing, does some processing with it, and then moves on to the next one. CoolThing is a fictitious scarce resourcethere are only three of them in the system. Its implementation is shown in Listing 2.
Listing 1 AllocExample.cs
using System; namespace AllocExample { class Class1 { [STAThread] static void Main(string[] args) { try { for (int i = 0; i <= 3; i++) { // allocate a Thing and use it CoolThing ct = new CoolThing(i); // do stuff with the Thing Console.WriteLine("Hello from Thing {0}", ct.ThingNumber); // Thing goes out of scope here and can be released } } catch (Exception e) { Console.WriteLine("Exception: {0}", e); } Console.WriteLine("Press Enter to exit program"); Console.ReadLine(); } } }
Listing 2 CoolThing.cs
using System; namespace AllocExample { public class CoolThing { private static readonly int MaxCoolThings = 3; private static int ThingsAllocated = 0; private int thingNumber; public int ThingNumber { get { return thingNumber; } } public CoolThing(int tn) { if (ThingsAllocated == MaxCoolThings) { throw new ApplicationException("No free CoolThings"); } thingNumber = tn; Console.WriteLine("Thing {0} allocated", thingNumber); ++ThingsAllocated; } ~CoolThing() { Console.WriteLine("Thing {0} released", thingNumber); --ThingsAllocated; } } }
The CoolThing constructor first checks whether any CoolThings are available. If so, it increments the number of allocated things and returns. If no CoolThings are available, the constructor throws an exception. The finalizer (destructor) decreases the number of allocated things, which simulates returning an item to a resource pool.
If you compile and run the program, your output will look something like this:
Thing 0 allocated Hello from Thing 0 Thing 1 allocated Hello from Thing 1 Thing 2 allocated Hello from Thing 2 Exception: System.ApplicationException: No free Things at AllocExample.CoolThing..ctor() in d:\allocexample\coolthing.cs:line 23 at AllocExample.Class1.Main(String[] args) in d:\allocexample\class1.cs:line 15 Press Enter to exit program Thing 0 released Thing 2 released Thing 1 released
Even though the allocated things go out of scope, no garbage collection occurs, so the finalizers are not called. The result? A resource leak that causes the program to crash.
This is a simplified example, but not a contrived example. A program that searches a directory for files that contain a particular text string would operate in much the same way, as would a program that allocates window handles (yes, you still can do that in .NET) or other Windows API objects.
So What's the Point?
The point is that programs that make use of objects that allocate scarce resources must take special care to ensure that the resources allocated by those objects are released as quickly as possible. But how? You can't call the object's finalizer, and forcing a garbage collection every time you release a resource would make your program unimaginably slow. The only reasonable solution is to create an object "de-initialization" method to release the scarce resource. The object itself remains intact, but the resource that it had allocated is returned to the resource pool. Listing 3 shows how this would be done.
Listing 3 Implementing a Disposal Method
private bool disposed=false; public void Dispose() { if (!disposed) { --ThingsAllocated; disposed = true; } } ~CoolThing() { Console.WriteLine("Thing {0} released", thingNumber); Dispose(); }
You would have to add some additional logic to every method that accesses the resource so that you could throw an exception if a program tries to access a resource that has been disposed. Modifying the loop in Listing 1 to use this new disposal method is very simple, as shown in Listing 4:
Listing 4 Using the New Dispose Method
for (int i = 0; i <= 3; i++) { // allocate a Thing and use it CoolThing ct = new CoolThing(i); try { // do stuff with the Thing Console.WriteLine("Hello from Thing {0}", ct.ThingNumber); // Thing goes out of scope here and can be released } finally { ct.Dispose(); } }