skip to content

New Memory Manager

The tracing garbage collector replaced the old freeze-and-own model, so shared mutable state across threads no longer throws. If your KMP knowledge predates that change, this is the topic that dates it.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What is the 'new memory manager' in Kotlin/Native, and what old model did it replace?

level: juniorimportance: must knowfreq 62%

answer

  1. Tracing GC replaced freeze()/ownership
  2. Default in 1.7.20, only option in 1.9.20
  3. Shared mutable state now allowed
  4. @SharedImmutable deprecated
  5. Still need synchronization for races

basics

~10 s

It is Kotlin/Native's modern garbage collector. It replaced an old rule that froze objects and locked them to one thread. Now you can share regular mutable objects across threads, like on the JVM.

solid answer

~40 s

The new memory manager (default since Kotlin 1.7.20, the only option since 1.9.20) is a tracing garbage collector for Kotlin/Native. It removed the legacy 'strict' model where objects were thread-confined and you had to call freeze() to share them, after which any mutation threw InvalidMutabilityException. Markers like @SharedImmutable and @ThreadLocal-driven workarounds are gone or deprecated. With the new manager, ordinary mutable objects can be referenced and mutated from multiple threads, so Kotlin/Native concurrency behaves much closer to the JVM. You no longer freeze top-level state, and coroutines work across threads without InvalidMutabilityException or IncorrectDereferenceException surprises. You still need normal synchronization (locks, atomics) to avoid data races; the GC removes ownership restrictions, not the need for correct concurrent code.

code

kotlin · 15 lines
kotlin
// Legacy Kotlin/Native (pre-new-manager)
import kotlin.native.concurrent.freeze

data class Config(var url: String)

fun legacyShare() {
    val cfg = Config("a").freeze() // deep-immutable
    // cfg.url = "b"  // -> InvalidMutabilityException at runtime
}

// New memory manager: no freeze needed, stays mutable
fun modernShare() {
    val cfg = Config("a")
    cfg.url = "b" // fine, even when shared across threads
}

go deeper

for a junior

Knows the new manager is a GC that replaced freeze() and now allows shared mutable objects across threads.

for a middle

Can name the legacy exceptions (InvalidMutabilityException) and that synchronization is still required.

for a senior

Recalls the version timeline (1.7.20 default, 1.9.20 only) and the deprecated annotations like @SharedImmutable/@ThreadLocal.

for a principal

Frames the migration impact for a shared KMP library and contrasts safety-by-construction (freezing) vs. GC + explicit synchronization tradeoffs.

## What the question is about Kotlin/Native compiles Kotlin to native binaries (iOS, macOS, Linux, Windows) with no JVM. It needs its own memory management. Historically it used a unique, restrictive model; the 'new memory manager' replaced it with a conventional garbage collector. ## The old (legacy) model Before the new manager, Kotlin/Native enforced **thread ownership** of objects: - Each object graph was 'owned' by the thread that created it. Another thread could not freely access it. - To share an object across threads you had to **freeze** it with the `freeze()` extension function, making the whole reachable graph deeply immutable. - Mutating a frozen object threw **`InvalidMutabilityException`**. - Accessing an unfrozen, foreign-thread object threw **`IncorrectDereferenceException`**. - Annotations like **`@SharedImmutable`** (a top-level `val` shared across threads, implicitly frozen) and **`@ThreadLocal`** (per-thread copy of top-level state) existed to work around these rules. This 'object freezing' model was safe-by-construction but painful: shared caches, singletons, and multithreaded coroutines were awkward, and many JVM-friendly libraries did not port cleanly. ## The new memory manager The new manager is a **tracing garbage collector** (stop-the-world mark phase, concurrent sweep in modern versions). Key effects: - **No more freezing.** `freeze()` is deprecated/no-op; objects stay mutable. - **Shared mutable state across threads is allowed.** A `var` in a top-level object can be read/written from multiple threads. - `InvalidMutabilityException` and `IncorrectDereferenceException` effectively disappear in normal code. - `@SharedImmutable` is deprecated; `@ThreadLocal` still exists but is rarely needed. ## Timeline (know roughly) - **1.6.0**: available behind a flag (preview). - **1.7.20**: **default**. - **1.9.20**: legacy manager **removed**; new manager is the only option. ```kotlin // Old world: this needed freeze() and any mutation later threw InvalidMutabilityException object Counter { var value: Int = 0 // legacy: mutating from another thread => error } // New world: ordinary mutable shared state is allowed across threads fun bump() { Counter.value += 1 // compiles and runs on any thread } ``` ## Important caveat The new manager removes **ownership** restrictions, not **concurrency correctness**. `Counter.value += 1` from many threads is still a **data race**. You must use proper synchronization — e.g. `kotlin.concurrent.AtomicInt`, `Mutex`, or platform locks. The GC frees you from freezing, not from thinking.

  • Does the new manager mean you no longer need synchronization?
    No. It removes thread-ownership/freezing, but concurrent mutation of shared state is still a data race. Use atomics, Mutex, or locks.
  • Which Kotlin version made the new manager the default?
    Kotlin 1.7.20 made it the default; 1.9.20 removed the legacy manager entirely.

The old model was a museum: to let others see an exhibit you sealed it in glass (freeze) forever. The new model is an open library: anyone can touch the books, but you still need rules so two people don't scribble at once.

saying these in an interview costs you the question

  • Thinking the new GC also handles thread synchronization automatically
  • Believing freeze() is still required to share objects
  • Confusing Kotlin/Native GC with the JVM's GC implementation
  • Saying it was always tracing and freeze() never existed
  • Claiming shared mutable state is impossible on Native

context

open as a page

Now that the new manager allows shared mutable state across threads, what tools do you use to keep that state correct, and what does the GC NOT do for you?

level: middleimportance: must knowfreq 44%

basics

~10 s

The garbage collector only manages memory; it does not stop two threads from changing the same data at once. You protect shared data with atomics, locks/mutexes, or by keeping it inside one thread.

open as a page

Explain freeze(), @SharedImmutable, and InvalidMutabilityException in the legacy model, and what happens to them under the new manager.

level: middleimportance: should knowfreq 48%

basics

~10 s

Old Kotlin/Native made you 'freeze' an object to share it across threads, which made it permanently read-only; changing it later crashed with InvalidMutabilityException. @SharedImmutable auto-froze a global. The new manager removes all of this.

open as a page

How does the new memory manager change multithreaded coroutines on Kotlin/Native, e.g. Dispatchers.Default and runBlocking with background work?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Before, coroutines on Native couldn't freely jump threads because shared objects had to be frozen. The new manager lets coroutines move state across threads normally, so multithreaded dispatchers like Dispatchers.Default work like on the JVM.

open as a page

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?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

The 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.

open as a page