Describe the GC characteristics of the new Kotlin/Native memory manager (collector type, stop-the-world, tuning, and interop leaks). When can you still leak memory?
answer
- Tracing GC, not ref-counting (legacy was ARC+cycle collector)
- Stop-the-world mark; CMS concurrent mode for shorter pauses
- kotlin.native.binary.gc=cms; GC.collect() to force
- Kotlin-only cycles collected automatically
- Leaks at Kotlin<->Swift/ObjC ARC boundary; use weak refs
basics
~20 sThe new manager uses a tracing collector that finds and frees unused objects, pausing threads briefly to scan. You can tune it with runtime settings. You can still leak memory through cycles that cross into Objective-C/Swift, or by holding references too long.
solid answer
~40 sThe new MM uses a **tracing garbage collector**: a mark phase that pauses application threads (stop-the-world) to trace reachable objects from roots, plus sweeping/freeing of the rest; modern versions add concurrent marking to shrink pauses. It is not reference-counted on the Kotlin side, so pure Kotlin reference cycles are collected. You can influence it via compiler/runtime flags and properties such as `kotlin.native.binary.gc=cms`, GC scheduler settings, and APIs like `kotlin.native.runtime.GC.collect()`. Leaks still happen at the **interop boundary**: Kotlin objects retained by Objective-C/Swift (ARC reference counting) and Swift objects retained by Kotlin can form cross-runtime cycles the GC cannot break — these need weak references or manual breaking. Long-lived roots (singletons, caches, unremoved listeners, global `@ThreadLocal` state) also leak logically. So: GC handles Kotlin-only garbage; the developer manages interop cycles and root lifetimes.
code
kotlin · 8 lines// Logical leak via a long-lived root the GC will never reclaim
object Registry {
private val listeners = mutableListOf<() -> Unit>()
fun add(l: () -> Unit) { listeners += l } // never removed -> leak
// fun remove(l: () -> Unit) { listeners -= l } // needed to avoid leak
}
// The GC cannot help: Registry is a root, so everything it holds stays alive.go deeper
Knows it's a garbage collector that frees unused objects automatically.
Identifies it as tracing (not ref-counting) and that holding references too long still leaks.
Explains stop-the-world/CMS, tuning flags like kotlin.native.binary.gc=cms and GC.collect(), and the ARC interop cycle leak.
Reasons about pause-time vs. throughput tradeoffs, profiling the interop boundary, and designing reference ownership across the Kotlin/Swift divide.
## Collector type The new MM is a **tracing GC**, not reference counting (the legacy MM used automatic reference counting + cycle collector). Tracing means: start from **roots** (stack variables, globals, thread-locals), follow references to **mark** reachable objects, then **sweep** the unmarked ones. - **Stop-the-world (STW):** application threads pause during certain phases (notably root scanning/marking). Newer Kotlin versions add a **concurrent mark** mode (CMS-style) to keep pauses short. - Cycles among **pure Kotlin** objects are reclaimed automatically (tracing handles cycles for free, unlike ref-counting). ## Tuning knobs (know they exist) Configured via Gradle `kotlin { ... }` binary options or runtime properties: - **`kotlin.native.binary.gc=cms`** — enable concurrent mark-and-sweep for lower pauses. - **GC scheduler / target heap** settings to control how aggressively GC runs. - **`kotlin.native.runtime.GC.collect()`** — request a collection manually (useful in tests/benchmarks). - Logging via runtime env vars to observe GC behavior. ```kotlin import kotlin.native.runtime.GC import kotlin.native.runtime.GCInfo @OptIn(kotlin.experimental.ExperimentalNativeApi::class) fun forceCollect() { GC.collect() // run a GC cycle now val last: GCInfo? = GC.lastGCInfo // inspect timings println(last) } ``` ## Where you can still leak 1. **Cross-runtime cycles (the classic Native leak).** iOS interop bridges Kotlin's tracing GC and Objective-C/Swift **ARC** (reference counting). If a Kotlin object holds a Swift object that holds the Kotlin object back, neither side can reclaim it — ARC sees a live reference, the GC sees an external root. Fix with `weak`/`unowned` on the Swift side or by breaking the reference (e.g. clearing a closure capture). 2. **Closures captured by Objective-C blocks** retaining `self`/Kotlin state. 3. **Long-lived roots:** singletons/`object`s, global caches, registered-but-never-removed listeners/observers, and `@ThreadLocal` state living as long as the thread. 4. **Native resources** (file handles, C pointers from cinterop) are not GC-managed memory and need explicit release. ## Mental model The GC guarantees: *unreachable Kotlin objects are reclaimed.* It does **not** guarantee: objects you keep reachable (roots) or that are pinned by another runtime (ARC) get freed. Profiling tools (Xcode Instruments, leak checkers) target the interop boundary precisely because that's where the two memory models meet.
- Why can a Kotlin/Native object leak even though the GC handles cycles?Because the cycle crosses into Swift/Objective-C ARC. ARC keeps the Kotlin object alive as an external reference, so the tracing GC sees it as reachable; break the cycle with a weak reference.
- How do you reduce GC pause times?Enable the concurrent mark-and-sweep mode (kotlin.native.binary.gc=cms) and tune the GC scheduler/heap targets; reduce allocation churn.
saying these in an interview costs you the question
- Saying the new MM uses reference counting like the legacy one
- Claiming the GC can break Kotlin<->Swift ARC cycles automatically
- Believing tracing GCs can't collect reference cycles
- Ignoring long-lived roots/listeners as a leak source
- Assuming GC frees native/C resources from cinterop