RAD Studio (Common)
ContentsIndex
PreviousUpNext
Memory Management Issues on the .NET Platform

The .NET Common Language Runtime is a garbage-collected environment. This means the programmer is freed (for the most part) from worrying about memory allocation and deallocation. Broadly speaking, after you allocate memory, the CLR determines when it is safe to free that memory. "Safe to free" means that no more references to that memory exist. 

This topic covers the following memory management issues:

  • Creating and destroying objects
  • Unit initialization and finalization sections
  • Unit initialization and finalization in assemblies and packages

In Delphi for .NET, a constructor must always call an inherited constructor before it may access or initialize any inherited class members. The compiler generates an error if your constructor code does not call the inherited constructor (a valid situation in Delphi for Win32), but it is important to examine your constructors to make sure that you do not access any inherited class fields, directly or indirectly, before the call to the inherited constructor.

Note: A constructor can initialize fields from its own class, prior to calling the inherited constructor.

Every class in the .NET Framework (including VCL.NET classes) inherits a method called Finalize. The garbage collector calls the Finalize method when the memory for the object is about to be freed. Since the method is called by the garbage collector, you have no control over when it is called. The asynchronous nature of finalization is a problem for objects that open resources such as file handles and database connections, because the Finalize method might not be called for some time, leaving these connections open. 

To add a finalizer to a class, override the strict protected Finalize procedure that is inherited from TObject. The .NET platform places limits on what you can do in a finalizer, because it is called when the garbage collector is cleaning up objects. The finalizer may execute in a different thread than the thread the object was was created in. A finalizer cannot allocate new memory, and cannot make calls outside of itself. If your class has references to other objects, a finalizer can refer to them (that is, their memory is guaranteed not to have been freed yet), but be aware that their state is undefined, as you do not know whether they have been finalized yet. 

When a class has a finalizer, the CLR must add newly instantiated objects of the class to the finalization list. Further, objects with finalizers tend to persist in memory longer, as they are not freed when the garbage collector first determines that they are no longer actively referenced. If the object has references to other objects, those objects are also not freed right away (even if they don’t have finalizers themselves), but must also persist in memory until the original object is finalized. Therefore, finalizers do impart a fair amount of overhead in terms of memory consumption and execution performance, so they should be used judiciously. 

It is a good practice to restrict finalizers to small objects that represent unmanaged resources. Classes that use these resources can then hold a reference to the small object with the finalizer. In this way, big classes, and classes that reference many other classes, do not hoard memory because of a finalizer. 

Another good practice is to suppress finalizers when a particular resource has already been released in a destructor. After freeing the resources, you can call SuppressFinalize, which causes the CLR to remove the object from the finalization list. Be careful not to call SuppressFinalize with a nil reference, as that causes a runtime exception.

Another way to free up resources is to implement the dispose pattern. Classes adhering to the dispose pattern must implement the .NET interface called IDisposable. IDisposable contains only one method, called Dispose. Unlike the Finalize method, the Dispose method is public. It can be called directly by a user of the class, as opposed to relying on the garbage collector to call it. This gives you back control of freeing resources, but calling Dispose still does not reclaim memory for the object itself - that is still for the garbage collector to do. Note that some classes in the .NET Framework implement both Dispose, and another method such as Close. Typically the Close method simply calls Dispose, but the extra method is provided because it seems more "natural" for certain classes such as files.  

Delphi for .NET classes are free to use the Finalize method for freeing system resources, however the recommended method is to implement the dispose pattern. The Delphi for .NET compiler recognizes a very specific destructor pattern in your class, and implements the IDisposable interface for you. This enables you to continue writing new code for the .NET platform the same way you always have, while allowing much of your existing Win32 Delphi code to run in the garbage collected environment of the CLR. 

The compiler recognizes the following specific pattern of a Delphi destructor:

TMyClass = class(TObject)
  destructor Destroy; override;
end;

Your destructor must fit this pattern exactly:

  • The name of the destructor must be Destroy.
  • The keyword override must be specified.
  • The destructor cannot take any parameters.
In the compiler's implementation of the dispose pattern, the Free method is written so that if the class implements the IDisposable interface (which it does), then the Dispose method is called, which in turn calls your destructor.  

You can still implement the IDisposable interface directly, if you choose. However, the compiler's automatic implementation of the Free-Dispose-Destroy mechanism cannot coexist with your implementation of IDisposable. The two methods of implementing IDisposable are mutually exclusive. You must choose to either implement IDisposable directly, or equip your class with the familiar destructor Destroy; override pattern and rely on the compiler to do the rest. The Free method will call Dispose in either case, but if you implement Dispose yourself, you must call your destructor yourself. If you want to implement IDisposable yourself, your destructor cannot be called Destroy.

Note: You can declare destructors with other names; the compiler only provides the IDisposable implementation when the destructor fits the above pattern.
The Dispose method is not called automatically; the Free method must be called in order for Dispose to be called. If an object is freed by the garbage collector because there are no references to it, but you did not explicitly call Free on the object, the object will be freed, but the destructor will not execute.
Note: When the garbage collector frees the memory used by an object, it also reclaims the memory used by all fields of the object instance as well. This means the most common reason for implementing destructors in Delphi for Win32 - to release allocated memory - no longer applies. However, in most cases, unmanaged resources such as window handles or file handles still need to be released.
To eliminate the possibility of destructors being called more than once, the Delphi for .NET compiler introduces a field called DisposeCount into every class declaration. If the class already has a field by this name, the name collision will cause the compiler to produce a syntax error in the destructor.

On the .NET platform, units that you depend on will be initialized prior to initializing your own unit. However, there is no way to guarantee the order in which units are initialized. Nor is there a way to guarantee when they will be initialized. Be aware of initialization code that depends on another unit's initialization side effects, such as the creation of a file. Such a dependency cannot be made to work reliably on the .NET platform. 

Unit finalization is subject to the same constraints and difficulties as the Finalize method of objects. Specifically, unit finalization is asynchronous, and, there no way to determine when it will happen (or if it will happen, though under most circumstances, it will).  

Typical tasks performed in a unit finalization include freeing global objects, unregistering objects that are used by other units, and freeing resources. Because .NET is a memory managed environment, the garbage collector will free global objects even if the unit finalization section is not called. The units in an application domain are loaded and unloaded together, so you do not need to worry about unregistering objects. All units that can possibly refer to each other (even in different assemblies) are released at the same time. Since object references do not cross application domains, there is no danger of something keeping a dangling reference to an object type or code that has been unloaded from memory. 

Freeing resources (such as file handles or window handles) is the most important consideration in unit finalization. Because unit finalization sections are not guaranteed to be called, you may want to rewrite your code to handle this issue using finalizers rather than relying on the unit finalization. 

The main points to keep in mind for unit initialization and finalization on the .NET platform are:

  1. The Finalize method is called asynchronously (both for objects, and for units).
  2. Finalization and destructors are used to free unmanaged resources such as file handles. You do not need to destroy object member variables; the garbage collector takes care of this for you.
  3. Classes should rely on the compiler's implementation of IDisposable, and provide a destructor called Destroy.
  4. If a class implements IDisposable itself, it cannot have a destructor called Destroy.
  5. Reference counting is deprecated. Try to use the destructor Destroy; override; pattern wherever possible.
  6. Unit initialization should not depend on side effects produced by initialization of dependent units.

Under Win32, the Delphi compiler uses the DllMain function as a hook from which to execute unit initialization code. No such execution path exists in the .NET environment. Fortunately, other means of implementing unit initialization exist in the .NET Framework. However, the differences in the implementation between Win32 and .NET could impact the order of unit initialization in your application.  

The Delphi for .NET compiler uses CLS-compliant class constructors to implement unit initialization hooks. The CLR requires that every object type have a class constructor. These constructors, or type initializers, are guaranteed to be executed at most one time. Class constructors are executed at most one time, because in order for the type to be loaded, it must be used. That is, the assembly containing a type will not be loaded until the type is actually used at runtime. If the assembly is never loaded, its unit initialization section will never run.  

Circular unit references also impact the unit initialization process. If unit A uses unit B, and unit B then uses unit A in its implementation section, the order of unit initialization is undefined. To fully understand the possibilities, it is helpful to look at the process one step at a time.

  1. Unit A's initialization section uses a type from unit B. If this is the first reference to the type, the CLR will load its assembly, triggering the unit initialization of unit B.
  2. As a consequence, loading and initializing unit B occurs before unit A's initialization section has completed execution. Note this is a change from how unit initialization works under Win32.
  3. Suppose that unit B's initialization is in progress, and that a type from unit A is used. Unit A has not completed initialization, and such a reference could cause an access violation.
The unit initialization should only use types defined within that unit. Using types from outside the unit will impact unit initialization, and could cause an access violation, as noted above.  

Unit initialization for DLLs happens automatically; it is triggered when a type within the DLL is referenced. Applications created with other .NET languages can use Delphi for .NET assemblies without concern for the details of unit initialization.

Copyright(C) 2008 CodeGear(TM). All Rights Reserved.
What do you think about this topic? Send feedback!