Garbage Collection and Object Lifetime

Garbage Collection and Object Lifetime

By Ric Parkin

Overload, 12(63):, October 2004


It seemed a simple bug report. "When we close the editing screen the framework asks the user to save the temporary portfolio we use internally as storage. Make sure it gets removed cleanly instead". "Should be easy," I thought. "We can't be leaking the objects, as the whole form and its helpers are written in C#. I just need to destroy the portfolio object at the right time."

The resulting solution opened my eyes to what Garbage Collection does and, more importantly, what it doesn't do. To understand this, we'll go back to the very basics of memory and object management, and see what various techniques are available. I'll concentrate on the C family of languages: C, C++, C# and the upcoming C++/CLI.

Memory and Object Lifecycle

The diagram below shows the stages in the life of memory and objects.

Raw memory is very simple: you acquire some from a pool of available memory, use it, and release it back to the pool to be reused. Failing to release it causes it to be considered "leaked".

Objects are slightly more complex because as well as obtaining the raw memory for their storage, they need to be initialised to a usable state and establish their class invariant, and have that state destroyed before releasing the memory. If an object is not destroyed then its state is considered leaked, which is important if that state is a scarce non-memory resource such as system file handles.

Let's look at some pseudo-code for creating and destroying an object, and then see how the C family languages map onto each part:

Memory_location memory_for_T
                = Acquire_Memory(size_of_T);
if(succeeded) {
  T_location T_object
                = Initialise_T(memory_for_T);
  if(not succeeded)
    Release_Memory(memory_for_T);
  else {
    // use T_object....
    Destroy_T(T_object);
    Release_Memory(memory_for_T);
  }
}
Memory and Object Lifecycle

Figure 1. Memory and Object Lifecycle

There are four situations I'll look at: creating an object on the stack; as a base class of some other object; as a member of an object; and on the heap. We'll illustrate these by considering a class described loosely as:

class B : A {
  C c;
  D* d;
}

We'll initialise each instance of A, B, C, and D with the numbers 1, 2, 3, and 4.

C++

C++ provides a very simple and clean solution

class B : public A {
public:
  B(int b_param)
     : A(1),
       c(3),
       d(new D(4)) {} 
private:
  C c;
  std::auto_ptr<D> d;
};
 
int main() {
  B b(2);
  // use b 
}

In C++, and all the other languages, the size of an object is worked out by the system, and takes into account all the space for the sub-objects.

If objects are constructed on the stack, then the memory is acquired and released automatically, often by just adjusting the stack pointer. Constructors play the part of the initialise function, and the programmer usually writes them, although there are cases where the compiler will generate them. When objects go out of scope the destructor is automatically called and the memory released.

Destructors are the destroy functions and the compiler automatically calls the destructors for base classes and members. It can even write them too - if there is nothing else needed other than the members' destructors to be called, then the required destructor is trivial, and the compiler will generate it for us if we leave it out altogether. Members of other objects are very similar to stack variables. In particular, when the outer object is destroyed, all its member objects are destroyed too.

Allocation on the heap is done using a "new-expression" which allocates memory and calls the required constructor. A "delete-expression" calls the destructor and frees the memory.

Experts at Kipling's game of "Kim" may have spotted something missing - error checking. Fortunately the compiler generates it all for you using exceptions. I'd have to be more careful if I had raw pointers in my class, but wrapping them up in auto_ptr makes that problem go away and I can be lazy and correct, which is every programmer's ideal.

C

C is more verbose - after all you have to do a lot more yourself. The only things the compiler will do for you is allocate and deallocate space on the stack and for struct members, and tell you the size needed for objects using the sizeof operator.

typedef struct B_tag {
  A a;
  C c;
  D* d;
} B;

D* D_new(int d_param) {
  void* memory = malloc(sizeof(D));
  if(memory == NULL)
    goto malloc_failed;

  D* d = D_init(memory, d_param);
  if(d == NULL)
    goto init_failed;

  return d;
 
init_failed:
  free(memory);

malloc_failed:
  return NULL; 
}
 
B* B_init(void* memory, int b_param) {
  A* a = A_init(memory, 1);
  if(a == NULL)
    goto A_failed;
 
  C* c = C_init(memory + offsetof(B, c), 3);
  if (c == NULL)
    goto c_failed;
 
  d = D_new(4);
  if(d == NULL) 
    goto d_failed;
 
  return (B*)memory;
 
d_failed:
   C_destroy(c);
c_failed:
   A_destroy(a);
A_failed:
   return NULL;
}
 
void* B_destroy(B* b) {
  // assume destruction can't fail
  free(D_destroy(b->d));
  C_destroy(&b->c);
  A_destroy((A*)b);
}

int main() {
  B b;
  B_init(&b, 2);
  // use b
  B_destroy(&b);
}

This is directly analogous to the C++ solution, and illustrates the sort of tricks the C++ compiler is doing behind the scenes, in particular the error checking and clearing up of partially constructed objects. But it is a lot of work. I'll leave it as an exercise for the reader to come up with a better solution in terms of writing and maintaining this sort of code.

What Is Garbage Collection?

Garbage Collection replaces manual releasing of memory such that 'leaked' memory is automatically reclaimed by the system and is then available for use.

It does this by finding objects that are no longer needed (technically, objects that are unreachable from "root" objects such as global variables and the stack by following member references) and reclaims their memory for future use. It can be compared to treating the program as having an infinite amount of memory - if you can never run out, then you don't need to bother to delete anything, and all objects can live forever and can be thought of as immortal [ Griffiths ].

It is very tempting, when starting to use garbage collection, to think that it means you don't have to worry any more about the tedious work of keeping track of object ownership and lifetimes, and the programmer can concentrate on more interesting and more productive work.

"... Garbage collection relieves you from the burden of freeing allocated memory ... First, it can make you more productive..." [ gc ]

"A second benefit of garbage collection ... is that relying on garbage collection to manage memory simplifies the interfaces between components ... that no longer need expose memory management details ("who is responsible for recycling this memory")." [ GC-faq ]

Unfortunately, this misses a subtle point in the relationship between ownership, object lifetimes, and memory management - they aren't the same thing. Garbage Collection frees you from having to clean up the memory, true. But Ownership and Lifetime still have to be carefully considered as part of design.

For example, holding on to object references for too long, or giving them to global objects, will keep them locked into memory. This is often referred to as a memory leak, although it is achieved by incorrectly holding onto things for too long, and not by forgetting to clean up as in C++.

C# and .Net

In C# construction is very much like C++ in that the new keyword combines allocating the memory and calling the constructor.

There is no explicit memory deallocation stage - that's done automatically by the Garbage Collector - but is there something that can destroy an object? Not for releasing memory for its members - again that's done by the Collector - but something for cleaning up non-memory resources at a specific time?

There is a special function called when an object is being reclaimed - the finalizer. At first sight this looks very much like a destructor (the C# syntax is the same, and the designers of Managed C++ in VC7 thought they were the same - MC++ destructors are actually the finalizer in disguise), but it has since become clear that the finalizer can't be used to destroy an object, for three reasons:

  • You don't know when it gets called. Things get finalized when the garbage collector runs, but all you know is that is may run at some unspecified point in the future, so you can't rely on it being called at a specific time

  • You don't know how many times it gets called, if at all. It's possible that the program finishes before the collector runs, in which case the finalizer is never called. Also, an object that has been finalized can be kept alive using the ReRegisterForFinalize method, and then finalized again. And again. And again.

  • You can't do much in it. When your finalizer is running, you don't know which other managed objects you have references to have already been finalized, so unless your design guarantees they're still living - in other words, you've carefully thought out their lifetimes - you can't touch any other objects. The only sensible thing you can do is to log some information somewhere to say it's been finalized.

It is sometimes recommended to use the finalizer to clean up important unmanaged resources that need to be released, such as handles from the operating system that the Garbage Collector doesn't know about. Unfortunately you may have run out of these resources before the collector runs and the finalizers get called, so you can't rely on that [ 1 ] .

Dispose

Recall in my original problem, that I needed to tidy up a particular object at a particular time. A common solution is to write a teardown method, and the .Net designers have provided a standard interface: IDisposable , which has a single method Dispose() to be called when you want the object to clean up and "die". However, as there can be other references to the object, Dispose may be called on an object multiple times, and it is also allowed that a disposed object may be reused, for example a disposed File Object could be reopened, and become "alive" again, but I suggest that this would get too confusing to recommend - keep it simple: Dispose destroys the object, and nothing else can use it afterwards.

Used like this, Dispose is a candidate for the equivalent of a destructor. If an object has resources that must be released at a specific time, implement Dispose and remember to call it. C# has even added help to the language to do this - using - which will automatically call Dispose on its argument at the end of a statement block, in a similar way to auto_ptr, or Boost's scoped_ptr.

So finally, here's the example in C#

We'll have our base class A inherited from a helper class Disposable - it's based on a pattern for writing disposable objects where both the Finalizer and the Dispose methods are dispatched to a single virtual helper [ msdn ]. Some classes in the .Net framework such as UserControl use this technique

public class Disposable : IDisposable {
  private bool isDisposed = false;
 
  public Disposable() {}
 
  ~Disposable() {
    Dispose(false);
  }
 
  public sealed virtual void Dispose() {
    if(!isDisposed) {
      isDisposed = true;
      Dispose(true);
      GC.SuppressFinalize(this);
    }
  }
 
  protected virtual void Dispose(
                         bool isDisposing) {}

  protected sealed void TryDispose(
                         Object object) {
    TryDispose((IDispose)object);
  }

  protected sealed void TryDispose(
                         IDispose idispose) {
    if(idispose != null)
      idispose.Dispose();
  }
}
 
public class B : A {
  public B(int b_param)
     : base(1) {
    try {
      c = new C(3);
      d = new D(4)
    }
    catch(Exception e) {
      Dispose();
      throw e;
    }
  }
 
  public override void Dispose(
                         bool isDisposing) {
    if(isDisposing) {
      // dispose of managed resources here
      TryDispose(d); d = null;   
      TryDispose(c); c = null;   
    } 
    // dispose of unmanaged resources here
    // and call the base class
    base.Dispose(isDisposing);
  }
  public static int main() {
    B b;
    using(b = new B(1)) {
      // use b;   
    } // b.Dispose called automatically
  }
}

Unfortunately using only works for objects whose lifetime is a local scope, but not for members, and they have to be cleaned up by hand.

C# doesn't allow objects embedded in other objects, only simple types and references to objects on the heap, so c has to be created on the heap, and this makes writing the constructor to cope with an exception being thrown more difficult.

Dispose has to be written by hand every time and if you forget to dispose of something, or it didn't used to be disposable but now ought to be, the resources haven't been disposed of at the right time.

An Improvement? C++/CLI

Microsoft is about to release their new attempt at getting C++ to work with CLI (the common language part of .Net). Its previous Managed C++ suffered from many problems, and is not widely used.

In this language, the solution can use many familiar C++ idioms:

ref class B : public A {
public:
  B(int b_param)
     : A(1), c(3), d(gcnew D(4)) {} 
 
private:
   int b;
   C c;
   auto_ptr<D> d;
             // write one for CLI references
};
 
int main() {
  B b(1);
  // use b
}

The destructors here are Dispose() , and the compiler is generating the implementation and the calls, just like C++.

I've assumed that there is an auto_ptr analogue that works with CLI references and the rest is just the slightly different syntax for creating an object on the managed heap.

Back To The Problem

In the original system, storage for financial instruments was managed by a simple Portfolio object, which had a Close method to tidy it up. An instance of this was shared between several Processor objects used to manipulate the portfolio, instances of which were in turn shared between several User Interface components.

The obvious first step was to make the Portfolio implement Dispose , and have that close the storage.

But it was not obvious who should be disposing of this object or when - there was no clear ownership and no notion of how long the object would remain usable for - one Processor could dispose of the Portfolio and the others could then try to use it again. My solution was to push the issue of ownership and destruction up a level, by making all the Processors that used the Portfolio themselves disposable, and documented that they could use the Portfolio given to them until they themselves were disposed of.

The User Interface objects were already disposable, so it was a simple matter to pass in the Processor they needed, and again define that they could use it throughout their own lifetime.

The top-level form created the Portfolio and Processors, hooked them up to the User Interface and set everything going. Finally, in response to the form needing to close, it was then a simple matter to dispose of all the User Interface objects, dispose of the Processors, and then dispose of the Portfolio.

So here we have an interesting consequence: if a resource must be cleaned up promptly, then every object that uses it needs to think about when it is no longer allowed to use it. In this case I did it by imposing a lifetime on the Processors and User Interface objects and guaranteeing that the Portfolio would outlive them.

The consequence of having a lifetime managed by calling Dispose has just spread from a low level helper tucked away in some other objects, all the way up to a top level object. It is very pervasive.

In this case, the solution resulted in virtually all non-trivial classes needing to implement Dispose , and involved a non-trivial amount of design rework to make the ownership relations and lifetime issues clear. The only classes that were not affected were very simple "value" types used to group together data items. The language and compiler provided no help as I had to write all the Dispose methods by hand, call Dispose for every non-trivial member, and hope that if a new member is added in the future or a class becomes disposable, then the writer remembers to update the Dispose method.

Conclusions

Far from Garbage Collection relieving the programmer of having to think about ownership and lifetime, these issues still exist in just the same way as in C++. Only relatively simple types have no need of the Dispose idiom and can be left to the collector - any type that uses, directly or indirectly, resources that need to be released in a timely fashion, needs to have their relative lifetimes thought about.

Current languages such as C# don't help the programmer in writing the mechanics of these things, but the forthcoming C++/CLI will bring many of the tools that C++ provides to improving this area.

References

[Griffiths] Alan Griffiths, "Heretical Java #1: Immortality - at a price", Overload 59

[gc] http://www.artima.com/insidejvm/ed2/gc.html

[GC-faq] http://www.iecc.com/gclist/GC-faq.html

[msdn] http://msdn.microsoft.com/library/en-us/cpref/ html/frlrfsystemidisposableclassdisposetopic.asp



[ 1 ] As Java's Garbage Collection is very like .Net's, this has led to some implementations of the Java library to try and get around this for file handles by triggering the garbage collector if an attempt to get a file handle fails, then trying again. This helps that particular program avoid running out, but may still be starving the system of the handles in the meantime.






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.