0

The following Kotlin Native test code uses weak references and manual triggering of garbage collection in the hope of ensuring objects have been reclaimed (rationale: if this works correctly then this mechanism can then be used in more complex scenarios to ensure various components don't hold to references they no longer need. Alternative approaches to achieve this goal are out of scope for this question and will not be accepted as answers, but are welcome in comments!):

import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame

class WeakReferenceTest {
    @Test fun weakReference() {
        var strong: Any? = Any()
        val weak = kotlin.native.ref.WeakReference(strong!!)

        kotlin.native.internal.GC.collect()
        assertSame(strong, assertNotNull(weak.get()))

        strong = null
        kotlin.native.internal.GC.collect()
        assertNull(weak.get())
    }
}

The problem is this test fails: even after losing the strong reference and triggering garbage collection, the last assertion still fails, stating that weak.get() is not null but rather still an Any object.

My question is why this test fails and how I can fix it, or at least how I can go about understanding what's going on there. For example, I don't know if the problem lies within the weak reference or with the garbage collection; is there maybe a way to check whether garbage collection really took place? Or is there a way to examine what the source code got compiled to, and see if there happens to be another reference generated by the compiler which is still in scope (in Why does some garbage not get collected when forcing a GC sweep in Kotlin JVM, depending on seemingly irrelevant factors?, for example, javap -c was used to detect such issues with Kotlin JVM).

Note that the exact same test code (only using the corresponding Java entities instead of the native ones) does pass on Kotlin JVM:

import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame

class WeakReferenceTest {
    @Test fun weakReference() {
        var strong: Any? = Any()
        val weak = java.lang.ref.WeakReference(strong!!)

        System.gc()
        assertSame(strong, assertNotNull(weak.get()))

        strong = null
        System.gc()
        assertNull(weak.get())
    }
}
Tom
  • 4,910
  • 5
  • 33
  • 48
  • I don't know Kotlin/Native, but if it behaves anything like the JVM, then you can't _force_ a garbage collection; `System.gc()` simply "_suggests_ that the Java Virtual Machine expend effort toward recycling unused objects…" (my emphasis) There are no guarantees. Depending on the type of GC, it may do a complete stop-the-world pass, or it may do one of a variety of possible partial collections; it may even start doing a big or small collection on other thread(s) and return before they've had a chance to do anything. You shouldn't rely on it. (See also [this question](/questions/66540).) – gidds Jul 20 '22 at 22:58
  • Thanks for your comment, @gidds. The documentation comment on `kotlin.native.internal.GC.collect` pretty much says that it _forces_ collection – not merely "suggests" as is the case on the JVM. Funnily enough, the test succeeds consistently on the JVM, but fails on Native... Maybe for whatever reason the collection doesn't actually take place and that's why the test fails – so I'd like to know if there's some tool/hack to allow me to monitor that and see whether/when garbage collection actually happens. Or maybe it needs time, or thread suspension or something? Does anyone know? – Tom Jul 21 '22 at 13:28

1 Answers1

0

Not the full answer I'd hope for, but better than nothing:

Refactoring the code so that the original referenced object only lives in another function makes the test pass:

import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame

class WeakReferenceTest {
    @Test fun weakReference() {
        val weak = generateAndVerifyWeakReference()
        kotlin.native.internal.GC.collect()
        assertNull(weak.get())
    }

    private fun generateAndVerifyWeakReference(): kotlin.native.ref.WeakReference<Any> {
        val data = Any()
        val weak = kotlin.native.ref.WeakReference(data)
        kotlin.native.internal.GC.collect()
        assertSame(data, assertNotNull(weak.get()))
        return weak
    }
}

This suggests that calling kotlin.native.internal.GC.collect() does indeed trigger garbage collection (and indeed without the call the test fails), and that the problem with the original version of the test is that the generated code keeps another, hidden reference to the object.

I'll be happy if someone can suggest a tool to inspect the generated code and see this more clearly and directly (like javap -c for the JVM case).

For completeness, here is the corresponding JVM version (which also passes):

import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame

class WeakReferenceTest {
    @Test fun weakReference() {
        val weak = generateAndVerifyWeakReference()
        System.gc()
        assertNull(weak.get())
    }

    private fun generateAndVerifyWeakReference(): java.lang.ref.WeakReference<Any> {
        val data = Any()
        val weak = java.lang.ref.WeakReference(data)
        System.gc()
        assertSame(data, assertNotNull(weak.get()))
        return weak
    }
}

Note: it's possible to replace the direct usages of the platform-specific WeakReference types and garbage collection functions with multi-platform expect class and expect fun declarations, with actual implementations for JVM and Native (unfortunately not for JS: JS does have a WeakRef type, but so far no standard way to trigger garbage collection).

Tom
  • 4,910
  • 5
  • 33
  • 48
  • Still wish to find some way to inspect what's going on and gain more insights, but for now I'm accepting this partial answer of mine, as it does provide a solution to the problem and as no other answers have been offered. – Tom Aug 23 '22 at 18:37