Author: Jens G. Balchen
Introduction
In modern Visual Basic, objects play a vital role. Since version 4.0, VB developers have had the opportunity to create and destroy objects at their own will. To understand why objects can and will cause memory leaks in Visual Basic, you have to understand how they work.
Creating the Object
Whenever you create an object in Visual Basic, you actually create two things -- an object, and a pointer (called an object reference). "VB does not use pointers", you might say, but that is not true. "VB does not let you manipulate pointers" is more precise. Behind the scenes, VB still makes extensive use of pointers.
The following code will create an object and a reference, both referred to as A:
Dim A As Class1
Set A = New Class1
To VB, this code reads something like this:
Create an object reference (pointer) called A to an object of type Class1
Create a new instance of Class1 and make A point to this instance.
If at some point you were to write
Dim B As Class1
Set B = A
what would have happened, is
Create an object reference (pointer) called B to an object of type Class1
Make B point to the same object instance as A.
If you modify B, A will have been modified as well, which is quite obvious since they refer to the same object.
Destroying the Object
To destroy an object in VB, you set it to Nothing. But wait a minute. If all we ever use are object pointers, how can we set the object itself to Nothing? The answer is: We can't.
When we set a reference to Nothing, something called the garbage collector kicks in. This little piece of software tries to determine whether or not an object should be destroyed. There are many ways to implement a garbage collector, but Visual Basic uses what is called the reference count method.
As I said earlier, all we ever use are references. When I execute the following code, I will not touch the object directly; instead, I will reset and destroy the object reference.
Dim A As Class1
Set A = New Class1
Set A = Nothing
When VB interprets the last line, it will remove the reference A. At this point, if the object has no more references, the garbage collector will destroy the object and deallocate all its resources. If any other references point to the same object, the object will not be destroyed.
Dim A As Class1
Dim B As Class1
Set A = New Class1
Set B = A
Set A = Nothing
At this point, the object will still be alive, since B holds a reference to it.
Fooling the Garbage Collector
The garbage collector is not really very smart, and it cannot keep up with the incredible pace at which humans produce errors. There is an easy way to trick the garbage collector into thinking an object should be kept alive indefinetely. This can best be illustrated with these two classes, Class1 and Class2.
' Class1 code
Option Explicit
Public A As Class2
Public Sub Class_Initialize()
Set A = New Class2
Set A.B = Me
End Sub
' Class2 code
Option Explicit
Public B As Class1
When an instance of Class1 is created, it will create an instance of Class2. This way, Class1 keeps a reference to Class2. But it will also set a reference in Class2... to itself. This way, Class1 referes to Class2, and Class2 referes to Class1. This is what we call circular references.
If I were to create and destroy an object of Class1, this is what I would do:
Dim MyObject As Class1
Set MyObject = New Class1
...
Set MyObject = Nothing
Behind the scenes, Class1 will have created another object, and set up a circular reference. When I destroy MyObject, I would expect the garbage collector to clean up. However, the garbage collector is left with the following situation:
- Object Class2 points to object Class1, so Class1 can't be destroyed even if MyObject is destroyed.
- Object Class1 points to object Class2, so Class2 can't be destroyed.
Oops.
Producing a Memory Leak
Modifying the above classes, it is very easy to produce VB code that will leak memory like crazy. The following code can be pasted into two classes and a form.
' Class1 code
Option Explicit
Private A As New Class2
Private Str As String
Private Sub Class_Initialize()
Set A.B = Me
' Allocate lots of memory
Str = Space(1024 ^ 2)
End Sub
' Class2 code
Option Explicit
Public B As Class1
' Form code
Private Sub Command1_Click()
Dim A As Class1
Do
' Will allocate 1 MB of memory.
Set A = New Class1
' Will leak 1 MB of memory.
Set A = Nothing
DoEvents
Loop
End Sub
On my development machine, I have 256 MB RAM and 256 MB virtual memory. The following image is taken from Task Manager after this code had been running for a while, and then stopped.
As you can see, the application is very efficient at leaking memory.
Avoiding the Problem
The obvious solution to this problem is avoiding circular references. However, this is not always a viable solution, since sometimes, circular references actually provide very useful functionality.
The best way to prevent this problem from arising is keeping a clear mind and adopting a structured way of working with objects. The following procedure could be inserted into any class, and should be modified to fit the needs of that particular class:
Public Sub Deallocate()
' Deallocate any object references and make sure they have a chance to
' deallocate theirs.
A.Deallocate
Set A = Nothing
B.Deallocate
Set B = Nothing
' ... etc...
End Sub
A more generic solution
If you want to provide some help to yourself and other programmers you work with, you could create an interface for this type of functionality.
Step 1: A template
Considering the previous Deallocate function, one could create a class called SafeObject that would contain this function.
' SafeObject code
Public Sub Deallocate()
End Sub
Step 2: Using the template
Other objects could implement this interface, like this:
' Class1 code
Option Explicit
Implements SafeObject
Private Sub SafeObject_Deallocate()
' Class1's special deallocation procedure.
End Sub
Step 3: New ways of destroying
The last step is to create a function in a module:
' mdlSafeObject code
Public Sub DestroyObject(o As Object)
Dim SafeO As SafeObject
' If this is a SafeObject, call Deallocate() first.
If TypeOf o Is SafeObject Then
' This is necessary to make VB recognise o as a SafeObject
Set SafeO = o
SafeO.Deallocate
End If
Set o = Nothing
End Sub
' Class1 code
Public B As Class2
Public C As Class3
Private Sub SafeObject_Deallocate()
' Destroy all references.
DestroyObject B
DestroyObject C
End Sub
If you always use the SafeObject interface, and always use DestroyObject instead of Nothing, all objects will at least have the ability to perform a safe deallocation of their references.