I have four objects that are all dependent on each other. They are CInvoice has many CInvoiceLines, CInvoiceLine has many CRoyaltyLines, CRoyaltyLine has many CRoyaltyAdjustments. In order to properly terminate all of these class modules, I need to remove any and all dependencies. If I don’t, the classes will stay alive and consume memory. With four levels like this eating memory, I will eventually have to answer the question: Do I want to send an error report?
I think I have my termination sequences right, but I’ll let you be the judge in the comments. I start in the CInvoice class, which I’ll call a Parent class. It has many children, including the CInvoiceLines class (a child collection) and each individual CInvoiceLine class (a child class). The CInvoiceLine class then becomes a parent class for my next round of terminations. His children are the CRoyaltyLines class and each instance of the CRoyaltyLine class.
All of my Terminate events are structured like
1 2 3 |
Public Sub Terminate() End Sub |
I don’t use the built-in class terminate event because it doesn’t fire at the right time. When I’m in a parent class, I do these three things:
- Call the Terminate method of the Child Collection Class
- Set the local Child Collection Class variable to Nothing
- Set the local Parent variable to Nothing
Items 1 and 2 are done in a parent class that is not also a child class. Item 3 is done in a child class that is not also a parent class. All three items are done in a class that is both a parent and a child. For instance, CInoviceLine is a parent with respect to CRoyaltyLines, but a child with respect to CInvoice, so all three steps must occur.
When I’m in a Child Collection Class (ex: CInvoiceLines), I do these three things:
- Call the Terminate method for each member of the collection
- Set the local Parent variable to Nothing
- Set the local Collection variable to Nothing
When item 1 is executed, it may be terminating a class that’s a parent class and the whole things starts over again. Here’s an example: In the CInvoice class
1 2 3 4 5 6 |
Public Sub Terminate() mobjLines.Terminate ‘Term the child collection class Set mobjLines = Nothing ‘term the local ccc variable End Sub |
Since mobjLines is a CInvoiceLines object, that Terminate method gets called first. In the CInvoiceLines class
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Public Sub Terminate() Dim i As Long ‘Terminate each member For i = 1 To mcolLines.Count mcolLines.Item(i).Terminate Next i Set mobjParent = Nothing ‘kill the parent variable Set mcolLines = Nothing ‘kill the collection variable End Sub |
When mcolLines.Item(i).Terminate
is called, it’s calling the Terminate method of a class that’s both a child and a parent. In CInvoiceLine
1 2 3 4 5 6 7 |
Public Sub Terminate() mobjRoyaltyLines.Terminate ‘Term the child collection class Set mobjParent = Nothing ‘kill the parent variable Set mobjRoyaltyLines = Nothing ‘kill the ccc variable End Sub |
The only difference between this and CInvoice is that I killed the parent variable because it’s also a child class. I won’t go through the rest, except to show you the last class, CRoyaltyAdjustment
1 2 3 4 5 |
Public Sub Terminate() Set mobjParent = Nothing End Sub |
Since this is a child class, but not a parent class, only killing the local parent variable is necessary.
Boy, there’s nothing more thrilling than terminating classes, is there? That’s why I added the image – to spice it up a little. For a little background, I’m abstracting my relational database into objects in VBA. That way I can reference clsInvoice.Lines(1).RoyaltyLines(2).Adjustments(3).Amount
, which I contend is easier to code and read. But the setup is a real pain.
Dick,
As far as I know there has never been a need to clean up memory with VB(A). Of course, I could be wrong about the historical need but there is absolutely no need to do any cleanup in todays environment. I suspect that is true for all languages except for unmanaged C++.
The Garbage Collector will do the needful.
And, I definitely don’t believe you should be creating and calling a public Terminate method. Terminate is a *private* method that is automatically invoked by VB.
In the following test, I nested an object of type Class2 in an object of type Class3. Each Class2 object had 500 nested object of type Class1, which in turn contained a string variable initialized with a string 1,000,000 characters long.
There was no cleanup code.
Memory usage on my PC went from about 766MB to about 1.77GB. I checked this when the VBA code reached the Stop statement. Then, with the next step, which terminated execution of the testMemory routine, memory usage dropped back to 766MB.
I ran the above test thrice. Very similar results each time.
Option Explicit
Dim aStr As String
Private Sub Class_Initialize()
aStr = String(1000000, “a”)
End Sub
‘Class2
Option Explicit
Dim anObj() As Class1
Private Sub Class_Initialize()
ReDim anObj(499)
Dim I As Integer
For I = LBound(anObj) To UBound(anObj)
Set anObj(I) = New Class1
Next I
End Sub
‘Class3
Option Explicit
Dim anObj2 As Class2
Private Sub Class_Initialize()
Set anObj2 = New Class2
End Sub
‘Module1
Option Explicit
Sub testMemory()
Dim x As Class3
Set x = New Class3
Stop
End Sub
My 2 cents:
>>As far as I know there has never been a need to clean up memory with VB(A)….The Garbage Collector will do the needful.
?, Surely not Tushar, even in .Net you stil need to call the GC, (often twice!) to control when it runs, if you leave it to the framework, you can be sure when it will run and clear your “dead” objects from memory.
In VBA I thought it was good practice to set all objects to Nothing when you have finished with them, thus removing them from the memory, but are you saying that VBA classes don’t need this? If the code keeps running then the memory will not be released? will it?
Interesting stuff…
I ran your code Tushar, my results where somewhat different. Looking at the task manager mem usage for Excel.Exe:
opened excel = ~40’000 k
run code = ~200’000 k
code stops = ~60’000 k
But there was large difference in the numbers.
look forward to seeing what other have to say.
Cheers
Ross
Tushar: If you put a Parent property in Class1 and set it equal to Class2, I think you will get different results. I agree as long as the dependency only goes one way, VB cleans itself up well. Make these changes
Public Parent As Class2
‘Class2
Private Sub Class_Initialize()
ReDim anObj(499)
Dim I As Integer
For I = LBound(anObj) To UBound(anObj)
Set anObj(I) = New Class1
Set anObj(I).Parent = Me
Next I
End Sub
The first blip is the original, the second is never ending.
Unless your Terminate methods are referring to global public original instances of the parent objects, I suspect your memory leak is akin to the difference between the two numbers in the last line of output from
Sub test()
Dim c1 As New Collection, c2 As Collection
Set c2 = c1
Debug.Print countof(c1), countof(c2)
c1.Add 1, “a”
c1.Add 2, “b”
Debug.Print countof(c1), countof(c2)
c2.Add 3, “c”
Debug.Print countof(c1), countof(c2)
Set c2 = Nothing
Debug.Print countof(c1), countof(c2)
End Sub
Function countof(x As Object) As Long
On Error Resume Next
countof = x.Count
End Function
That is, if your variables representing objects in your Terminate calls are just REFERENCES to those objects rather than the original objects themselves, setting them to nothing only frees the reference, not the original object.
Your Parent properties are causing circular references. These don’t get cleared until the original process is reset. In effect, that’s a memory leak.
The VB workaround is an absolutely beautiful hack first demonstrated, I believe, by Bruce McKinney. First, change your internal variable for the parent object to a long. This is going to point to the object rather than refer to it. Then use the undocumented objptr and the hack objfromptr functions to convert the object to and from its pointer. This way you’re never storing an object reference, so the circular reference in not created.
Private m_lngParentPtr As Long
Private Declare Sub CopyMemory Lib “kernel32” Alias “RtlMoveMemory” _
(dest As Any, Source As Any, ByVal bytes As Long)
‘ The Parent property
Public Property Get Parent() As Class1
Set Parent = ObjFromPtr(m_lngParentPtr)
End Property
Public Property Set Parent(obj As Class1)
m_lngParentPtr = ObjPtr(obj)
End Property
‘Returns an object given its pointer.
‘This function reverses the effect of the ObjPtr function.
Private Function ObjFromPtr(ByVal pObj As Long) As Object
Dim obj As Object
‘ force the value of the pointer into the temporary object variable
CopyMemory obj, pObj, 4
‘ assign to the result (this increments the ref counter)
Set ObjFromPtr = obj
‘ manually destroy the temporary object variable
‘ (if you omit this step you’ll get a GPF!)
CopyMemory obj, 0&, 4
End Function
HTH
Rob
Freakin’ brilliant Rob.
It might be brilliant, I can’t tell. My brain hurts trying to follow this thread.
Dick: Yes, you are absolutely correct. Implementing a parent property requires some additional work to release the memory. I went back and looked at some data structures with two-way object references that I’d implemented over the past few years. While that code still looks good, I will have to remember the issue raised by this discussion.
Rob’s post about the hack should prove valuable for those implementing a two-way object reference. I assume it would work in the example I shared in my first post in this discussion.
Excellent, thanks for the solution. Key missing command in my code was the 2nd CopyMemory to destory the temporary memory (I thought that Set obj = Nothing was good enough). My error would occur after running the CopyMemory and then exiting Excel. 5 – 10 secs later, it would give me a “Microsoft Excel has stopped working” error message.