|
|
||
|
|
||
In this chapter, we have examined how assemblies are implemented and gone over the process of creating a complex, multi-module strongly named assembly that has satellite resource assemblies. In more detail, we have
Finally, we have gone over the rich model for localizing resources, and presented a couple of examples that
|
|
||
|
|
||
|
|
||
Memory management, and in particular the cleaning up of any resources that are no longer needed by an application, is an important service provided by the .NET Framework. The aim of this chapter is to make sure you understand at a
The main advantages of garbage collection versus alternative techniques; in short, why Microsoft decided on garbage collection as the best solution
The way the garbage collector works, including details of the algorithm it uses at present, and the way it hijacks your threads
Implementing Dispose() and Finalize()
Weak references
We'll start the chapter by investigating the advantages of garbage collection as compared to the alternatives.
For most of the book, the
term resource indicates some specific unmanaged or managed resource that is embedded in or linked to an assembly, such as a bitmap. However, in this chapter, we use the term resource more generically to mean any item that occupies memory or system resources. This could be, for example, a plain managed object, or on the unmanaged side it could be an unmanaged object or an external resource such as a file or window handle. We should also differentiate between 'normal' resources - such as managed memory -for which the GC'sfinalization mechanism is adequate, and 'precious' resources, such as database connections, which really need to be cleaned up as soon as the program has finished with them.
|
|
||
|
|
||
|
|
||
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
Garbage collection.
The system used in .NET, in which some specialist piece of software examines the memory in your process space and
.NET, of course, has gone for a primarily
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
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 (
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
As far as fears of objects hanging around too long are
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
The advantage of this system has traditionally been performance. As the theory goes, the developer knows his own source code, and probably
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
// 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
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
Incidentally, when we come to examine the Dispose() method, we will discover similar difficulties, though to a much lesser extent.
Reference counting was the favored solution in COM, and, while it proved useful in COM, has two problems: performance and cycles.
Look at any typical large application, and you will find object references (or in unmanaged C++, pointers) being
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
Cyclic references
are the other big problem with reference counting. Cyclic references don't happen that often, but when they do, they can prevent
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
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
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
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 .
|
|
||