Search code examples
kotlingarbage-collectionweak-referenceskotlin-native

Testing against memory leaks in Kotlin Native


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())
    }
}

Solution

  • 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).