skip to content

Describe the Kotlin/Native memory model and garbage collector. What changed from the original (legacy) model, and what does it mean for sharing objects across threads?

level: middleimportance: should knowfreq 40%

answer

  1. Tracing mark-and-sweep GC, increasingly concurrent
  2. New model default since 1.7.20
  3. Legacy: freeze(), InvalidMutabilityException, @SharedImmutable
  4. Now share+mutate across threads like JVM
  5. Use atomics / @Volatile / @ThreadLocal for low-level

basics

~10 s

Modern Kotlin/Native uses a tracing garbage collector and lets you share regular objects freely across threads, like the JVM. The old model that froze objects and forbade sharing is gone.

solid answer

~40 s

Kotlin/Native ships its own runtime with a **tracing garbage collector** (a stop-the-world mark-and-sweep that has become increasingly concurrent across releases). Since Kotlin 1.7.20 the **new memory model** is the default: objects can be shared and mutated across threads without the old restrictions. The **legacy model** required objects passed between threads to be **frozen** (deeply immutable via `freeze()`), threw `InvalidMutabilityException` on mutation, and exposed `@ThreadLocal`/`@SharedImmutable` annotations and `AtomicReference`/`FreezableAtomicReference` to work around it. Those are now deprecated. Practically, this means Kotlin coroutines and shared state work the same way as on the JVM, easing multiplatform code. You still don't get JVM semantics for everything — e.g., there's no `synchronized` keyword bytecode-level guarantee identical to JVM, and `@Volatile`/atomics (`kotlinx.atomicfu` or `kotlin.concurrent.atomics`) are the recommended tools for low-level concurrency.

go deeper

for a junior

Knows Kotlin/Native has a garbage collector and that objects can be shared across threads now.

for a middle

Explains the move from the freezing legacy model to the new default model and names freeze()/InvalidMutabilityException.

for a senior

Discusses GC characteristics (tracing, concurrency), remaining race risks, and atomics/@Volatile usage.

for a principal

Reasons about porting strategy, concurrency design across KMP, and the historical cost of the legacy model on adoption.

## The runtime and the GC Kotlin/Native links a small **runtime** into every binary. That runtime includes a **garbage collector (GC)** — automatic memory management that reclaims unreachable objects. The GC is a **tracing GC** (it walks reachable object graphs from roots) using **mark-and-sweep**; newer Kotlin releases made it progressively more **concurrent** (less stop-the-world pause time). There is no JVM here, so this GC is Kotlin/Native's own implementation. ## Legacy memory model (historical) Before the new model, Kotlin/Native enforced strict thread isolation: - An object reachable from more than one thread had to be **frozen** — made deeply, permanently immutable via the `freeze()` function. - Mutating a frozen object threw **`InvalidMutabilityException`**. - Special tools existed to cope: **`@ThreadLocal`** (per-thread global state), **`@SharedImmutable`** (frozen global), **`AtomicReference`/`FreezableAtomicReference`** for shared mutable references, and **`Worker`** for concurrency. - This made porting JVM/coroutine code painful. ## New memory model (default since Kotlin 1.7.20) - Regular objects can be **shared and mutated across threads** without freezing — much like the JVM. - `freeze()`, `@SharedImmutable`, and the freezing exceptions are **deprecated / no-ops**. - Coroutines and `kotlinx.coroutines` behave consistently with other targets, removing a major multiplatform friction point. ## Low-level concurrency tools today Even with shared mutability, you must still avoid data races: ```kotlin import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.ExperimentalAtomicApi @OptIn(ExperimentalAtomicApi::class) val counter = AtomicInt(0) fun bump() { counter.fetchAndAdd(1) } ``` - Use **atomics** (`kotlin.concurrent.atomics`, or the multiplatform `kotlinx.atomicfu` library) and **`@Volatile`** for visibility. - `@ThreadLocal` still exists for genuinely per-thread state. ## Why it matters The shift from freezing to a JVM-like shared model is the single biggest usability improvement for Kotlin/Native and Kotlin Multiplatform — shared business logic written for the JVM now runs on iOS without redesigning state ownership.

  • Why was the old freeze-based model considered a barrier to Kotlin Multiplatform adoption?
    It forced developers to redesign shared mutable state and made coroutines behave differently than on the JVM, so common JVM code didn't port cleanly to iOS.
  • Does the new model mean you no longer need to think about thread safety?
    No — you can now create data races, so you must use atomics, @Volatile, locks, or confinement just as on the JVM.

The legacy model was like having to laminate any document before handing it to a coworker; the new model lets you pass live, editable documents around.

saying these in an interview costs you the question

  • Claiming Kotlin/Native still requires freezing objects to share them
  • Saying there is no GC in native binaries
  • Believing the new model makes all code automatically thread-safe
  • Confusing @ThreadLocal with @SharedImmutable semantics
  • Thinking the memory model is identical to the JVM in every detail

context