What is the expected behavior of garbage collectio...
# coroutines
d
What is the expected behavior of garbage collection across suspend boundaries in suspending functions? In the following example I would expect the object of type
MyCustomType
to be garbage collected immediately after
callThis
completes (as happens without coroutines), however this does not happen.
Copy code
class MyCustomType(val str: String)

    suspend fun callThis() {
        val type = MyCustomType("Test")

        delay(100)

        println(type.str)
    }

    suspend fun secondCall() {
        delay(100)
    }

    @Test
    fun `Testing coroutines`() {
        runBlocking {
            callThis()

            secondCall()
        }
    }
Looking at the IntelliJ Memory profiler, the object is not removed until after
secondCall
. Im wondering why this memory needs to persist into the execution of the parent function? The value does not get garbage collected until after the next suspend call or after the
runBlocking
block has finished. In a scenario where a suspending function like this uses a lot of memory across coroutine boundary and then passes execution to a blocking program (still inside runBlocking) I could see a memory leak being caused, though I might be missing something.
y
Try manually triggering a GC after callThis, you'll see that it doesn't leak. The reason likely is that, without
suspend
, the JIT realizes that
type
never escapes its context, and hence removes it immediately, while with
delay
, the object does get stored somewhere (namely in the state machine for
callThis
), and hence such an optimization doesn't happen immediately. This shouldn't affect anything at all though
y
I don't think that's what they're observing here. Nothing should be holding onto the
callThis
state machine instance between
callThis
and right before
secondCall
. This would only apply basically if
callThis
was inlined. Also, that issue is solved, and I'm pretty sure I've observed correct behaviour in this circumstance in my experiments.
e
I'm not saying it's that issue, I have a longer comment coming…
🤦🏼 1
with normal code, the JVM will allow objects to be garbage collected even if there are still local references to them, as long as no further code will use them. (otherwise long-running functions would hold onto garbage longer than desired). and in this case, since it's only local, it never gets a written reference from anywhere else and even without escape analysis, it should be easily cleaned up by nursery collection
with suspend, Kotlin needs to spill locals into the state machine across suspend points. Kotlin nulls out those references afterwards to maintain similar behavior (except when given a compiler argument to not do that to make some debugging easier), but 1. this is most definitely "escaping" the function call from the JVM's perspective, and 2. if the state machine has been promoted out of the nursery, then this write also makes the object ineligible for nursery collection
1
d
@Youssef Shoaib [MOD] I figured something similar about the gc call, but I tried this it appears that the objects do not get collected even at the
println
method in this example:
Copy code
@Test
    fun `Late night test`() {
        runBlocking {
            callThis()

            System.gc()

            println("Here") // Object still in memory
        }
    }
Is this what you meant?
Anyways, thank you for the responses. This definitely clears up some confusion I had, and also its just far more likely I have some other memory leak in my larger program...
My annoyance is that analyzing the references here I cannot find any reference to the top level object besides in coroutines that should have already been garbage collected, and I see only circular references to themselves