Управление памятью в Kotlin: как работает сборщик мусора

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Память и сборщик мусора в Kotlin: как не слить RAM и не потерять производительность

Вы когда-нибудь задумывались, куда исчезает оперативная память вашего приложения на Kotlin? Вроде бы код написан идеально, корутины летают, а через час работы программа начинает «тормозить» или падает с OutOfMemoryError. Знакомая боль? Добро пожаловать в клуб тех, кто хочет понять, как Kotlin управляет памятью и почему сборщик мусора — это не зло, а ваш лучший друг (если знать, как с ним дружить).

В этой статье мы нырнем в тему управления памятью в Kotlin и разберем, как работает сборщик мусора в Kotlin на разных платформах. Вы узнаете, что такое стек и куча, как избежать утечек, и почему WeakReference может спасти ваш проект. Поехали!

Что такое память в Kotlin? Стек, куча и их друзья

Kotlin — мультиплатформенный язык. Он компилируется в байт-код JVM, в нативный код (через Kotlin/Native) или в JavaScript (Kotlin/JS). И на каждой платформе память устроена немного по-своему, но база везде одинаковая.

Давайте вспомним азы. Когда ваша программа запускается, операционная система выделяет ей область памяти. Внутри этой области есть два главных «района»:

  • Стек (Stack) — здесь хранятся локальные переменные, примитивы и ссылки на объекты. Он работает как стопка тарелок: быстро, предсказуемо, но с ограниченным размером. Выход за пределы стека — StackOverflowError.
  • Куча (Heap) — свалка для всех объектов, которые вы создаете через new или конструктор (val list = mutableListOf()). Куча большая, но медленная, и именно в ней «живет» сборщик мусора Kotlin.

На JVM стек и куча работают классически. В Kotlin/Native стек — это нативный стек потока, а куча управляется собственным сборщиком мусора. В Kotlin/JS — все ложится на плечи JavaScript-движка (V8, SpiderMonkey).

Ключевой момент: ссылки на объекты хранятся в стеке, а сами объекты — в куче. Пока на объект есть хоть одна «живая» ссылка (из стека или другого объекта), сборщик мусора его не тронет.

Сборщик мусора в Kotlin: как он работает на JVM

Когда вы пишете на Kotlin под JVM (Android, сервер), вы получаете мощь Java Virtual Machine Garbage Collector (GC). Это не что-то специфическое для Kotlin, но понимать его механику необходимо.

Современная JVM использует G1GC (Garbage First) или ZGC (в новых версиях). Как это работает упрощенно:

  1. Mark (Маркировка) — GC «проходится» по всем живым объектам, начиная от корневых точек (стек, статические поля). Все, до чего он добрался — живо.
  2. Sweep (Очистка) — всё, что не отмечено, считается мусором и удаляется.
  3. Compact (Компактизация) — живые объекты сдвигаются, чтобы не было фрагментации.

В G1GC куча разбита на регионы. GC собирает те регионы, где больше всего мусора (отсюда и название). Это снижает паузы, но не исключает их полностью.

В Kotlin вы можете влиять на сборщик мусора через флаги JVM (для серверов) или через GarbageCollectorMXBean (для мониторинга). Но чаще всего вам нужно просто писать код, который не мешает GC работать.

Пример: как GC видит ваш код

fun processData() {
    val list = mutableListOf<String>()
    for (i in 1..100_000) {
        list.add("Element $i")
    }
    // Здесь list — жив, пока метод не завершится
    println(list.size)
    // После выхода из метода list становится мусором
}

После завершения processData() ссылка на list исчезает из стека, и объект становится кандидатом на сборку. Просто, правда?

Особенности Kotlin/Native: сборщик мусора без JVM

Kotlin/Native — это отдельная вселенная. Здесь нет JVM, поэтому сборщик мусора в Kotlin/Native — это собственная реализация, основанная на подсчете ссылок (Reference Counting) с ленивым циклическим сборщиком.

Как это работает:

  • Каждый объект имеет счетчик ссылок. Когда вы присваиваете ссылку — счетчик растет, когда теряете — падает.
  • Когда счетчик достигает нуля — объект удаляется мгновенно. Это круто для производительности, но есть нюанс: циклические ссылки.

Циклическая ссылка — это когда объект A ссылается на B, а B ссылается на A, и оба не нужны. Счетчики никогда не обнулятся. Для этого в Kotlin/Native есть фоновый циклический сборщик (Cyclic Collector), который запускается по таймеру или при нехватке памяти.

Пример циклической ссылки в Kotlin/Native

class Node(var next: Node? = null)

fun createCycle() {
    val a = Node()
    val b = Node()
    a.next = b
    b.next = a  // Цикл!
    // После выхода из функции a и b не могут быть удалены сразу
}

На JVM такой цикл соберется, потому что GC использует маркировку. В Kotlin/Native — только когда циклический сборщик проснется. Поэтому для Kotlin Native memory важно избегать циклов, если вы пишете высоконагруженный код.

Kotlin/JS: память под капотом браузера

В Kotlin/JS управление памятью полностью отдано на откуп JavaScript-движку. Там используется поколенческий сборщик мусора (Generational GC). Молодые объекты (новые) собираются часто, старые — реже.

Ваша задача как разработчика — не создавать лишних объектов в горячих циклах и не удерживать ссылки на DOM-элементы после того, как они удалены из документа. Иначе получите утечку памяти в Kotlin в браузере.

Как избежать утечек памяти в Kotlin? Практические советы

Независимо от платформы, есть общие принципы, которые помогут вам держать память под контролем.

1. Используйте WeakReference для кэшей и слушателей

Если вы храните ссылку на объект, который может быть удален (например, Activity в Android), используйте WeakReference. Это позволит GC

Блоги

Рекомендаций по книгам