Object not being deleted - Why?

BlitzMax Forums/BlitzMax Programming/Object not being deleted - Why?

Gabriel(Posted 2007) [#1]
This question is a bit vague I suppose, but I can't really post all the involved source, so vague is as good as it's going to get.

My game objects have a Destroy() method which cleans up after them. I think I do this correctly, but it's always possible I've made a mistake, so I write a little debug note in there to help me spot any circular references.

At the end of my Destroy() method I have this code :

GCCollect() ' MAKE SURE OBJECTS WE DESTROYED ARE THEMSELVES COLLECTED AND LOSE THEIR REFS
DebugLog Byte Ptr(Self)[- 4] + " REFERENCES REMAIN"


If this is ever greater than one, there is a circular reference I haven't broken. If it's one ( that reference being the one that called this destroy() method in the first place ) then it's clean as a whistle.

My object clears this hurdle, passing back a 1. This sole reference is a local variable, used to cycle through all the objects in a TMap. I've even confirmed 100% for certain that this TMap does not contain the object. As soon as this local variable goes out of scope, the object *must* be ready for collection, right?

Wrong. It never gets collected. I call GCCollect in my main loop ( only for ease of debugging purposes, of course ) and the delete method is never called. This makes no sense to me. I'm confirming the 1 reference, I know what that reference is, and I know that 1 reference can't last, but it's still not being deleted.

The only possible ( logical ) conclusion is that I create another reference after calling the destroy method and before the local variable goes out of scope. The only problem with that theory is that the loop through all the objects in the TMap does nothing else. It simply calls an update method and the call to destroy ( if executed ) is right at the end of the destroy method. Nothing else is executed.

What possible thing have I missed? I can see no logical reason for the object not being collected. Is there a flaw in my logic anywhere?


LarsG(Posted 2007) [#2]
I thought it was adviced against running a GCCollect in your main loop?
I recall reading somewhere that you should limit it to manually collect after intensive loops with buffer variables etc..

I can't say I have tried it though...


Gabriel(Posted 2007) [#3]
As I said, it's only there for situations like this where I'm debugging.


fredborg(Posted 2007) [#4]
Erm, the destroy method itself references the Self object. It won't go out of scope until the method has returned.

Or maybe I misunderstood something?


Gabriel(Posted 2007) [#5]
That's the one I'm already accounting for. By my understanding Self objects aren't reference counted separately, only from the variable which initially calls them, which is why I described it was being held in a local variable which loops through a TMap. Wherever you consider it, I'm counting it.

My question is why it isn't being collected when that one reference goes out of scope.


fredborg(Posted 2007) [#6]
How can you count the references to it without having a reference to it?


Gabriel(Posted 2007) [#7]
You can't. That's why I've said twice now that it should be destroyed when the reference count hits one. Because I'm using a local variable to get that reference count and that will go out of scope almost immediately afterwards.


fredborg(Posted 2007) [#8]
You should find this interesting:
Type TAnnoying

	Global list:TList = New TList

	Method New()
		list.AddLast(Self)
		Print "alive"
	EndMethod
	
	Method Kill()
		list.Remove(Self)
		DebugLog Byte Ptr(Self)[- 4] + " REFERENCES REMAIN"
	EndMethod

	Method Delete()
		Print "dead"
	EndMethod

EndType

Global k:TAnnoying = New TAnnoying
k.Kill()
k = Null

GCCollect()

End
If k is Local then the reference never dies because k remains in scope until program termination.

Which may or may not be a bug...


Gabriel(Posted 2007) [#9]
That's because K isn't being constantly reassigned to a new object. As I said, the local variable which is holding the last reference to my object cycles through every object in a TMap, so it's constantly changing. It also has a completely different scope to my variable, which is within a function.

I can only conclude that this hack to get BlitzMax's internal reference count is now broken.


Gabriel(Posted 2007) [#10]
If you want an example of the scope I actually have and why it should be going out of scope, here is one. The scope of everything is identical.


Type Test
	
	Field Name:String
	Field V:Int
	
	Global Map:TMap=New TMap
	
	Method Delete()
		RuntimeError "BING!"
	End Method
	
		
	Function Create(I:Int,sName:String)
		Local T:Test=New Test
		T.V=I
		T.Name=sName
		Map.Insert(T.Name,T)
	End Function
	
	Function Update()
		For Local E:Test=EachIn Map.Values()
			If E.V=2
				E.Destroy()
			End If
		Next
	End Function
	
	Method Destroy()
		Map.Remove(Self.Name)
	End Method
	

End Type



Test.Create(1,"One")
Test.Create(2,"Two")
Test.Create(3,"Three")
Test.Create(4,"Four")
Test.Create(5,"Five")


Test.Update()


GCCollect


Delay 1000




Note that the delete method here *is* called.


Brucey(Posted 2007) [#11]
If this is ever greater than one, there is a circular reference I haven't broken.

Surely if you don't know if you've broken a circular reference or not, you don't really know how your program is working properly?


Gabriel(Posted 2007) [#12]
Surely if you don't know if you've broken a circular reference or not, you don't really know how your program is working properly?

What I know and what I can prove are two different things. If I post on here and say I know I'm breaking circular references, ten people will say "I bet you're not." If I show code which proves it, I hoped that could be avoided.


gman(Posted 2007) [#13]
it may or may not help, but something that i learned a while back is that GCCollect() is not immediate.

http://www.blitzmax.com/Community/posts.php?topic=63550


Gabriel(Posted 2007) [#14]
Is that not the same thing that Fredborg posted? Your local is a global local ( local to the whole program, if you see what I mean ) whereas this is a function local, and the structure is as I showed above, which if you run it, does collect it.


fredborg(Posted 2007) [#15]
Now I'm completely confused... If it's collected then what is the problem you are having?


Gabriel(Posted 2007) [#16]
It's not collected in my game. It is collected in the example above which uses the same way of triggering the destructor, since you were suggesting that the way I called the destructor might be causing the last reference to go out of scope.


JoshK(Posted 2007) [#17]
I spend more time chasing memory leaks now than before GC.


Gabriel(Posted 2007) [#18]
Well I've honestly been pretty happy with the GC in general, but I've always felt it was underpowered, and needed improving. Weak references and the ability to retrieve a reference count are basic features of most managed-memory environments. Now we seem to be going backwards instead of forwards. Either the GC is broken or the reference count hack is broken. I don't much care which it is at this point, although the latter is obviously infinitely preferable in a wider sense, but either way, it makes the GC even less use than it was in the past, and it wasn't good enough in the first place, so it certainly can't afford to be getting worse.


Dreamora(Posted 2007) [#19]
Have you a code showing the actual problem you are facing.
Potentially you are using some chain calling to that method which might show similar issues to the ones suggested above but due to a different root.

Haven't had any freeing problems so far I've to say.
The only one I tend to run into quite too often is when I use TLinks on classes for fast removal from the list again and forget to make my destroy method null-ifying the tlink. in that case the object will never be freed.


Oh and just to make sure: global variables operate on the reference count differently than locals ... means they don't imidiate free


Gabriel(Posted 2007) [#20]
Have you a code showing the actual problem you are facing.

No, and I'm afraid I don't have the luxury of time to be able to strip my game apart and figure out why the GC is spazzing over it. I know I'm handling my objects correctly, so either the object is being collected without the delete method being called ( if so, no big deal ) or the reference count hack is broken ( if so, no big deal ) or the GC is rarely failing to collect objects ( if so, it's probably rare enough that it won't matter, and the objects are not using much memory. )

Potentially you are using some chain calling to that method which might show similar issues to the ones suggested above but due to a different root.

I'm afraid the chain is exactly as I showed it in the sample above, and it works in the sample above. I can't say why it doesn't work in the game, but I've wasted too much time trying to understand something which is never going to make sense, so I'm just going to have to trust that it won't become a bigger problem than it currently is.


TaskMaster(Posted 2007) [#21]
I found that if I had TLists in my classes, when I killed my class, sometimes the class would not get collected. The TLists should have gone out of scope and got collected, but they didn't. So, in my Destroy function, I erased any TLists. My classes then got collected.

This didn't always happen, just sometimes. And it may be because I left a reference to the TList somewhere else (but I don't think so).

Anyway, if you try clearing any TLists in your classes in the Destroy function, the problem may go away.


Dreamora(Posted 2007) [#22]
Gabriel: I understand that you are frustrated.
But I hope you understand as well that the hacks you used (we were talking on this topic when we were looking for ways of weak reference with auto collect when the library external references have been removed) might simply be interfering somewhere as well.

[0] and [4] of objects are private data and not meant for you to be used nor can you trust the data which are in there.
It was less of a problem in 1.24 but if you updated to 1.26 you will most likely run into bad problems as the reflection stuff access these fields as well.


marksibly(Posted 2007) [#23]
Hi,

The GC mechanism is tricky, and the ref counts wont always give a clear indication of what's going on.

The way the GC works, Max can optimize away pretty much all 'local' refs to objects, so a '1' ref count actually/usually implies there is a global (or field, or array element - ie: a memory ref) ref left.

There are other states a ref count can be in too - if the high bit is set, then it's a 'floating' ref - there are no global refs left and it's on an internal stack for (possible) later collection.

And this is all of course subject to change - if I get round to restoring the ability to debug while executing Delete() methods, this will probably require yet another internal stack somewhere. Play around with ref counts at your own peril!

As always, the acid test is: does your program leak memory over time?