Why Use Garbage Collection?

When it comes to cleaning up resources, there are broadly three options available:

  • Developer-controlled. The C/C++ model in which the developer explicitly indicates in the source code when a resource is no longer required and should be deleted, using a statement similar to delete in C++.

  • Reference counting. This is the model used in COM and COM+, and in VB6, although it is often best suited to a client-server environment. Objects are instantiated by clients, and it is the responsibility of the client to inform the object when it is no longer required. The object deletes itself when it detects that it is no longer required by any client.

  • Garbage collection. The system used in .NET, in which some specialist piece of software examines the memory in your process space and removes any object that it detects is no longer referenced.

.NET, of course, has gone for a primarily garbage-collected model, but with a developer-controlled element that is available when required, via the IDisposable interface.

The algorithms used by the .NET GC are also sometimes referred to as 'exact'. 'Exact' garbage collection is guaranteed to release the memory held by all dead objects. This is contrasted with older 'conservative' garbage collection algorithms, which were simpler but couldn't always distinguish between pointers and other data types, and therefore weren't guaranteed to release all unused memory.

Exact garbage collection has the potential to be an extremely efficient mechanism. However, it does of course impose some requirements on the system - it can only work if every object is self-describing - in other words, if there is some way for the garbage collector to obtain details of the object's size and field layout, including which fields refer to other objects. Otherwise, the garbage collector has no way of working out from the state of the process's memory which objects are still being referred to.

In the particular case of .NET, this information is available because each object contains a field at a known location that points to other structs that contain this information (specifically, the first word in the object points to a method table, which in turn points to an EEClass structure that holds the information). Garbage collection does also have a possible disadvantage in that it does not support deterministic finalization. Deterministic finalization means that an object will be destroyed and any associated destruction/finalization code will be executed at a well-defined point in time. When coding an application, deterministic finalization is significant because it means that when you write code elsewhere in the application, you know whether a certain finalizer will have executed already.

Note that the terms 'finalizer' and 'destructor' both indicate a method that is responsible for cleaning up an object (performing finalization or destruction). However, such a method is usually termed a finalizer if it is normally invoked by a garbage collector, and a destructor if it is normally invoked in some other means.

The lack of deterministic finalization did cause quite a few heartaches in the developer community when .NET was first released, for a couple of reasons. In the first place, there was concern that, on the face of it, it would seem rather hard to write code if you don't know what other finalization code will have already been executed. And there was also concern that there would be an unacceptable claim on resources due to objects that were no longer needed still being around waiting for garbage collection. In practice, both of these fears have proved unfounded:

  • Not knowing the order and timing in which finalizers are executed in practice only affects code written inside other finalizers. And the only restriction imposed by this lack of knowledge is that you must not write code that depends on the finalizer for any other object having been or not been called, or on the existence or not of any other managed object. Since, as we will see later in the chapter, the purpose of finalizers is as a last point of call to clean up unmanaged objects there is absolutely no reason why finalizers should contain code references to other managed objects anyway. So this restriction is a moot point.

  • As far as fears of objects hanging around too long are concerned, this would be an important issue if garbage collection were the only means available to delete objects. But Microsoft has anticipated the need for a deterministic element in the way in which certain objects are cleaned up, and provided this via an optional IDisposable interface, which objects can implement if needed. If an object references a number of managed or unmanaged resources, and it is important that those resources are cleaned up as soon as possible, that class can simply implement IDisposable, thus allowing clients' code to explicitly indicate when that object is no longer required. Of course, this opens up the possibility for bugs in which the client code fails to call Dispose() appropriately - but that situation is no worse than for the other memory management models: in the C/C++ model, there is the risk of the developer forgetting to call delete on a dynamically allocated object, while in the COM reference counting model, there is an equal risk of the developer of client code forgetting to insert the call to IUnknown.Release(). Indeed, the situation is arguably worse in those two models than the situation in a garbage-collected environment, since the consequences of bugs in the client are more serious. For the C/C++ models and for reference counting, such a bug will lead to a memory leak that will persist until the process ends, whereas with the garbage-collected model, there is a good chance that the 'leak' (although not any unmanaged resources) will be removed earlier, during a future garbage collection.

The history of the debate on .NET garbage collection is quite interesting. When the .NET Framework was first released, there was a fair discussion on the various .NET newsgroups, with a number of developers concerned that memory management was going to suffer because of the lack of deterministic finalization. As the months went on, this debate gradually died down and the .NET garbage collection model became accepted. With hindsight, it was apparent that deterministic collection is something which, if you're used to having it, is very easy to convince yourself that it's an absolutely vital part of your programming armory. However, in practice, once you've got used to doing without it, you find that it really makes no difference except on very rare occasions (which are covered by IDisposable). If you are interested in the arguments in detail, it's worth having a look at the web page http://discuss.develop.com/archives/wa.exe?A2=ind0010A&L=DOTNET&P=R28572&I=3. This page consists of a detailed analysis by one of the lead programmers at Microsoft, Brian Harry, of the arguments that eventually persuaded Microsoft that garbage collection was the way forward. In this chapter we won't go into too much detail about the arguments, but we will summarize the advantages and disadvantages of the various systems.

C/C++ Style Cleanup

The advantage of this system has traditionally been performance. As the theory goes, the developer knows his own source code, and probably knows when data is no longer needed far better than any compiler or environment could figure out. And he can hard-code that information into the program, via delete or equivalent statements, so the environment doesn't need to do any work whatsoever to figure out what needs deleting when. This means that there is no overhead from any algorithms to work out when to delete objects, and also that resources can be freed virtually the instant that they are no longer used.

The disadvantage is the extra work the developer has to do and the potential for hard-to-find memory leak bugs, as well as bugs in which objects are deleted too soon - which can lead to memory corruption issues in release builds that are often hard to track down.

In general, if your code has been designed in such a way that there is an obvious life span-containment relationship between classes, then writing cleanup code is easy. For example, suppose we have an EmployerRecord class, which contains references to name and job description classes, and for some reason you need all these objects to be individually allocated dynamically.

 // This is C++ code! class EmployerRecord {    private:       Name *pName;       JobTitle *pTitle; 

Assuming the Name and JobTitle instances can only be accessed through the EmployerRecord, cleanup logic simply involves propagating the delete operations:

 ~EmployerRecord {    delete pName;    delete pTitle; } 

However, suppose we have a second way of accessing the names. Suppose that there is another class, EmployerNameList, which contains pointers to all the Name instances.

Now the process of, say, deleting an employer is a lot harder, because you need to track the deleted name through to the EmployerNameList and amend that object. And suppose the EmployerNameList offers the option to delete an employer too - now the EmployerRecord destructor might have to figure out whether it's been invoked from the EmployerNameList or from elsewhere. You might think that's a bad architecture in this particular example, but this does illustrate the kinds of problems you get manually writing destruction code if you have a complex set of interrelated objects. In this situation, it is easy to see how destruction-related memory bugs can occur.

Incidentally, when we come to examine the Dispose() method, we will discover similar difficulties, though to a much lesser extent.

Reference Counting

Reference counting was the favored solution in COM, and, while it proved useful in COM, has two problems: performance and cycles.

Performance

Look at any typical large application, and you will find object references (or in unmanaged C++, pointers) being copied around everywhere. Without reference counting, copying a reference involves just that: copying a number from one memory location to another memory location. With reference counting, you have to check that the reference is not null. If it isn't, you need to de-reference it and increase that object's reference counter. And if you want to set a reference to null, or if the reference goes out of scope, you need to check whether the reference wasn't already null, and if it wasn't, decrease the reference count. If that kind of logic has to happen every time you do the equivalent of a = b; or a = null; for managed reference types, application performance will clearly suffer heavily. So on performance grounds alone, reference counting was never going to be an option.

Incidentally, as far as COM was concerned, reference counting was a suitable solution. But that's because reference counting in COM was done in a very particular client-server context. Essentially, there were two big blobs of code - the client code and the COM object (server) code, and reference counting only happened for objects used across the boundary. Within each blob, it was likely to be C/C++ style memory management that was used - this minimized the performance hit, but was a particular artefact of the COM environment, and it would be unlikely that the same benefits would occur had reference counting been used as the memory management solution for managed code.

Cyclic references are the other big problem with reference counting. Cyclic references don't happen that often, but when they do, they can prevent deactivation of objects. Imagine, for example, that object A holds a reference to object B, B has a reference to C, and C a reference to A. The whole cycle was originally established due to another object, X, which was responsible for instantiating A. Now suppose this outside object (X) no longer requires A, and so reduces its reference count and releases A. This means that none of the objects A, B and C are required any more, so they should all be removed. The trouble is that A refuses to remove itself, because it thinks it is still needed: C still has a reference to A. Meanwhile, B refuses to die because A still exists, and C clings on to life on the grounds that it is still required (by B). We're stuck! The workaround for this situation was complex, involving something called a weak reference (no relation to the .NET weak reference, which we will explore later and which has a different purpose). The cyclic reference problem would be a serious issue if reference counting were to be implemented in .NET. In COM, because of the client-server model, cyclic references tended not to occur quite as often, although even in COM the issue was a serious one.

Before leaving the topic of reference counting, we ought to note that there are two variants of reference counting: auto-reference counting, as used in VB6, and reference counting with smart pointers, as used for example in the ATL CComPtr class. Both of these variants work on the same principles as COM reference counting. However, with auto-reference counting, the compiler works out where reference counts need to be decremented or incremented, without any programmer involvement. This takes the programmer error element out of reference counting, but leaves the other disadvantages (as well as a potentially greater performance hit since the compiler might not notice some reference counting optimizations that the developer can see). Smart pointers are designed to give a similar effect to auto-reference counting, but work instead by classes defined in the source code that wrap pointers to reference-counted objects. The smart pointer classes automatically deal with the reference counts in their constructors and destructors.

Garbage Collection

Now we've seen the disadvantages that led Microsoft to avoid either reference counting or manual memory cleanup as the solution to resource management in .NET - and garbage collection is the only option left. But you shouldn't take that in a negative light. Garbage collection does actually have a lot going for it in principle, and Microsoft has done a lot of work to provide a good quality, high-performance garbage collector. For a start, as we've seen, garbage collection largely frees the developer from having to worry about resource cleanup. Given that one of the main goals of .NET was to simplify programming, that's important. Unlike reference counting, there's virtually no overhead with garbage collection until a collection has to occur - in which case the GC pauses the program, usually for a tiny fraction of a second, to allow the collection. And the really great thing about garbage collection is that it doesn't matter how complex your data structures are, or how complex the pattern of which objects link which other objects in your program. Because the sole test for whether an object can be removed is whether it is currently accessible directly or indirectly from any variables in current scope, the garbage collector will work fine no matter how objects are linked together. Even cyclic references are no problem and do not require any special treatment. Consider for example, the cyclic situation we discussed earlier in which objects A, B, and C maintained cyclic references to each other, with an outside object X holding a reference to A. As long as X holds this reference, the garbage collector will see that A is accessible from the program, and hence so are B and C. But when X releases its reference to A, there will be no references in the program that can be used to access any of A, B, or C. The garbage collector, when next invoked, will see this and hence will know that all of A, B, and C can be removed. The fact that these objects contain cyclic references is simply irrelevant.

Another big possible advantage of garbage collection is the potential to create a more efficient heap. This is because the garbage collector has an overall view of the heap and control of the objects on it in a way that is not possible with either reference counted or C/C++ style heaps. In the case of the CLR, the garbage collector takes advantage of this by moving objects around to compact the heap into one continuous block after each garbage collection. This has two benefits. Firstly, allocating memory for an object is very fast, as the object can always be allocated at the next free location. There is no need to search for free locations through a linked list, as is done with C/C++ style heaps. Secondly, because all the objects are compacted together, they will be closer together, which is likely to lead to less page swapping. Microsoft believes that as a result of these benefits, a garbage-collected heap may ultimately be able to out-perform a C++ style heap, even though maintaining the latter ostensibly seems to require less work. However, each pinned object (fixed in C#) will prevent this compaction process - which is the reason that pinning is regarded as such a potential performance issue.

The disadvantage of garbage collection is of course the lack of deterministic finalization. For the rare cases in which resource cleanup needs to be done sooner than the garbage collector will do it, Microsoft has provided the IDisposable interface. Calling IDisposable.Dispose() works in much the same way as calling delete in unmanaged C++, with the difference that you don't need to call Dispose() nearly as often -just for the objects that implement it, not for every object. And if you do forget to call Dispose(), the garbage collector and the Finalize() method are there as a backup, which makes programming memory management a lot easier and more robust than in C++.

Strictly speaking, the analogy between Dispose() and unmanaged delete is only approximate. Both Dispose() and delete call finalization code, which may cause contained resources to be cleaned up, but delete also destroys the object itself, freeing its memory - Dispose() does not do this. In terms of implementation, a better analogy is between Dispose() and explicitly calling an unmanaged destructor in C++, but in terms of usage, Dispose() really replaces delete.



Advanced  .NET Programming
Advanced .NET Programming
ISBN: 1861006292
EAN: 2147483647
Year: 2002
Pages: 124

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net