Are finalisers called when the last reference is garbage collected, or when Any reference is garbage collected?

Viewed 124

The julia docs say:

finalizer(x, f)

Register a function f(x) to be called when there are no program-accessible references to x. The type of x must be a mutable struct, otherwise the behavior of this function is unpredictable.

However, I seem to be observing that they are triggering when the first reference to my type loses its last reference.

Consider:

using Base.Test
mutable struct Foo
    val
end
@testset "how do Finalisers work" begin let
    fin_calls = []

    a = Foo(1)
    b = a
    finalizer(a, _ -> push!(fin_calls, "a"))
    finalizer(b, _ -> push!(fin_calls, "b"))

    @test fin_calls == []
    a = 7
    gc()
    @test fin_calls == [] # Fails: Evaluated: Any["a", "b"] == Any[]
    #shouldn't trigger finaliser as still has 1 ref, or so I thought

    b=8
    gc()
    @test Set(fin_calls) == Set(["a", "b"]) #both finalizers should trigger
end end

There are 2 references to my Foo(1), those being a and b

I would have expected that:

  • When a is changed to something else, nothing happens
  • When b is also changed so something else, both registered finalizers trigger. Causing "a" and "b" both to be added to the fin_calls array.

Instead, what I am observing is:

I would have expected that:

  • When a is changed to something else, both registered finalizers trigger. Causing "a" and "b" both to be added to the fin_calls array.
  • When b is also changed so something else, nothing further occurs.

What is the correct way to understand what is going on? This is in julia 6.0

My ability to reproduce this seems varied.

  • On original machine it showed for 0.6.0.
  • On another machine:
    • 0.6.1 (-O2 default ) resulted in a failure of the finaliser not being called (which is OK, it is ok for finaliser to be called late).
    • 0.7-dev (-O0) resulted in a pass,
    • 0.7-dev (-O1 or higher) resulted in Failure due to both being called.
1 Answers

If you do not use a variable, The garbage collector is free to collect a variable at any time after it is last used.

If you do not use a variable at all then the optimizer is free to never generate it in the actual code at all.

I believe that is what has occurred. If I add some more uses of the variables the the behavour is as expected.

using Base.Test

"This function makes use of `xs` in a way no optimizer can possibly remove"
function use(xs...)
    mktemp() do fn, fh
        print(fh, xs)
    end
end


mutable struct Foo
    val
end

function foo()
    @testset "how do Finalisers work" begin let
        fin_calls = []

        a = Foo(1)
        b = a
        finalizer(a, _ -> push!(fin_calls, "a"))
        finalizer(b, _ -> push!(fin_calls, "b"))

        use(a,b)
        @test fin_calls == []
        a = 7
        gc()
        use(b)
        @test fin_calls == [] # Fails: Evaluated: Any["a", "b"] == Any[]
        #shouldn't trigger finaliser as still has 1 ref, or so I thought

        b=8
        gc()
        @test Set(fin_calls) == Set(["a", "b"]) #both finalizers should trigger
    end end

This code, for me gets to the final test and then fails as at that point fin_calls has never been called. because the garbage collector can choose not to run.

end

Related