Why golang's garbage collector didn't free obj when an finalizer is associate with it?

398 views
Skip to first unread message

Soren Yang

unread,
Nov 5, 2023, 10:01:14 AM11/5/23
to golang-nuts
As shown in the following code:


The s1 & s2 didn't been free, and finalizer didn't run. But when enable the line which have been commented, all run as expected(s1 & s2 been free)。

I have seen the comment in runtime.SetFinalizer: If a cyclic structure includes a block with a finalizer, that cycle is not guaranteed to be garbage collected and the finalizer is not guaranteed to run, because there is no ordering that respects the dependencies.

But why it haven't been free in first code?

j2gg0s

unread,
Nov 6, 2023, 3:29:47 AM11/6/23
to golang-nuts

Just my(newbie of GC) guess:

In the mark stage,
- GC find s1's finalizer and invoke scanobject to scan s1
- then find s2 and queue it to be scanned
- then find s1

Avoid SetFinalizer reference s1 

Jan

unread,
Nov 6, 2023, 5:01:04 AM11/6/23
to golang-nuts
I was very surprised by this behavior of SetFinalizer: and indeed if you remove the SetFinalizer one can see that s1 is freed, because the memory reported by `m.HeapAlloc` goes back down.

I think you already have the answer: the block that has the cycle (s1 and s2) have a SetFinalizer set, and it will never run, per documentation (I had never paid attention to this).

A suggestion to work around would be to move the stuff that needs a "Finalizer" to a leaf node, as in:


But I understand this is not a solution if the finalizer needs to access the S1 (so, if the finalizer function needs any information that is not self-contained in `S1Data` in my example).

Jan

unread,
Nov 6, 2023, 5:09:50 AM11/6/23
to golang-nuts
For what it's worth, a bit of "memory management" on structures in many cases is very ok (not sure if in your case). So for your cyclic structure with finalizers, requiring the user of your code to call some "Finalize()" method (some destructor method you define) that manually breaks the cycle, often is an ok solution. Fundamentally, it's the same as requiring someone to call Close() on an opened file (or any other resource, like sockets, db connections, etc).

As an anecdote, previously when I was doing C++ I was a big fan of referenced counted smart pointers (`shred_ptr<>`), which gets most of the benefit of GC, but with a much lower cost. They required manually breaking cycles, which I didn't find to be an issue at all in the great majority of the cases.

Harish Ganesan

unread,
Nov 6, 2023, 10:20:39 AM11/6/23
to golang-nuts
Does this behaviour mean that, those memory will never be freed and keep piling on ? That can be disastrous.

Michael Knyszek

unread,
Nov 6, 2023, 9:51:58 PM11/6/23
to golang-nuts
Yes, cycles containing a finalizer aren't guaranteed to be freed. As others have pointed out, this is documented. SetFinalizer is really designed for a narrow set of use-cases, so concessions were made for overall GC performance. This case is one of them.

IIUC, the core problem is a combination of the fact that the cycle can be arbitrarily deep and the GC necessarily has to keep referents of the object-to-be-finalized live even if the object isn't referenced anymore. The GC must follow the pointers in an object's referents, and eventually it may come upon the almost-dead object it started from. But at that point it likely has no knowledge of where it came from. It's just a pointer on a queue. A GC could be implemented that keeps track of where the pointers it follows came from, but such an implementation would be substantially less performant.

Other GCs make the same choice. See the Boehm collector, for example.

Jan

unread,
Nov 7, 2023, 2:32:28 AM11/7/23
to golang-nuts
Btw, I don't follow the sentence:

"the GC necessarily has to keep referents of the object-to-be-finalized live even if the object isn't referenced anymore"

That is true for objects not in cycles that need to be finalized as well. I'm not sure I follow the reasoning here ...

Asking about it in Bard, it explains:

In Go, objects that are in a cyclic structure and that are marked with a finalizer (with SetFinalizer) don't get garbage collected when there are no more live pointers to the cyclic structure because the garbage collector cannot determine a safe order to run the finalizers.
"

Which seems to match the  Boehm collector explanation, described under "Topologically Ordered Finalization".

Harish Ganesan

unread,
Nov 7, 2023, 9:33:16 AM11/7/23
to golang-nuts
I can completely understand the logic behind not freeing the objects with unclear ordering. But I expected that there will be some kind of upper limit / workaround for these kind of objects. But this means that using SetFinalizer on a big object, carelessly, can potentially lead to memory leaks.

I guess if we provide a destructor( like file.Close() or something like that ) for those objects, which just breaks the cycle, it will probably be enough to force the GC to clear the memory.

Michael Knyszek

unread,
Nov 7, 2023, 9:51:51 AM11/7/23
to golang-nuts
On Tuesday, November 7, 2023 at 2:32:28 AM UTC-5 Jan wrote:
Btw, I don't follow the sentence:

"the GC necessarily has to keep referents of the object-to-be-finalized live even if the object isn't referenced anymore"

That is true for objects not in cycles that need to be finalized as well. I'm not sure I follow the reasoning here ...
That fact alone isn't enough. When you couple that fact with the fact that cycles can be arbitrarily deep, then you have a problem.

Sorry, this is perhaps a weird framing since it's based on the implementation details. Where I'm starting from is the example at the top of this post, which is a cycle that has only a single finalizer. The point I'm trying to make is that even having just a single finalizer in a cycle is a problem. AFAICT, the dependency rules that both the Go GC and the Boehm GC state could still be respected if each strongly connected component of objects had just a single finalizer, since that entire component could be finalized and freed before the next, and the ordering of finalizers is preserved. (If I'm wrong about this point, disregard me. I'm pretty sure but not certain that one could design a GC that did this.) But having to identify such connected components would be a significant performance limitation.

Asking about it in Bard, it explains:

In Go, objects that are in a cyclic structure and that are marked with a finalizer (with SetFinalizer) don't get garbage collected when there are no more live pointers to the cyclic structure because the garbage collector cannot determine a safe order to run the finalizers.
"

Which seems to match the  Boehm collector explanation, described under "Topologically Ordered Finalization".
That is also true, but AFAICT only really applies to cycles containing more than one finalizer. If a cycle (and I think more specifically, a strongly connected component of objects) has just one finalizer, one could argue such an ordering problem doesn't exist (unless I'm missing something of course). Nonetheless, the cycle still isn't collected.

Jan

unread,
Nov 8, 2023, 2:50:04 PM11/8/23
to golang-nuts
That makes sense that if you have only one finalizer in the cycle, the ordering could be respected, thanks for pointing it out!

Now I still don't understand your explanation. The fact that the cycles can be arbitrarily deep is the same for normal GC, and it works without an issue. But I don't know how the GC works in Go -- last I read about GC was mark&sweep :) Any pointers to somewhere I could read about it ? 

I just re-read A Guide to the Go Garbage Collector, but it doesn't cover our topic -- also I find it odd that it talks about CPU costs, as opposed to memory bandwidth costs, since (I imaging) the mark phase time must be dominated by memory bandwidth due to the random access (as opposed to the sweep that I assume is sequential).

cheers

ps.: I also asked Bard and ChatGPT, but neither provided a good explanation.
Reply all
Reply to author
Forward
0 new messages