Про сборщик мусора сейчас почти не говорят, при этом я часто слышу от коллег, что на собеседованиях про него спрашивают. И ещё заметил, что это один из популярных вопросов. Нормальных статей или обзоров про это почти нет, а если и есть, то они тяжёлые и очень нагруженные технической частью, которую понимают только сами создатели статей.

Эта статья — моя попытка объяснить вам доходчивым языком, что же такое Garbage Collector в Dart, объяснить почему эти некоторые знания нам нужны на практике и что из всей этой тяжелой, технической внутрянки вам необходимо знать.

Эта статья опирается на документацию Вячеслава Егорова о внутренностях Dart VM. Я пересказываю её простым языком и добавляю то, что важно на практике.

Биты наше все

Да, все настолько будет разжевано, что придется вспомнить основы основ. Что же такое биты и зачем они нам:

Если брать простой пример примитивный, то память компьютера можно визуализировать как длинный ряд ячеек, внутри каждой такой ячейки лежит бит, а бит это 0 или 1.
С битами процессор работает не по одному, а группами. И если мы возьмем «современные» 64-битные процессоры, то увидим, что у все они работают с группами битов размером 64 бит(8 байт). Одну такую группу можно называть машинным словом.
Указатель же в Dart помещается ровно в это одно машинное слово, то есть 8 байт.

Итак, два важных утверждения из описанного:

  • Бит 0 или 1 

  • Указатель = 8 байт.

Что такое указатель

Указатель это не сам адрес, а место, где этот адрес записан. Возьмем аналогию на примере адреса дома. Листок это указатель, а то, что на нем написано — это и как раз и есть адрес. Когда программе необходимо узнать адрес, она берет указатель и считывает адрес. По этому адресу мы дальше узнаем что же за объект нас ждет и объект ли это вообще.

Адрес может ссылаться не на объект?

Может. И это гениальное решение. В коде мы используем разные данные. Разного вида числа и объекты. 

Возьмем пример: 

int age = 7;

User user = User();

Dart хранит эти две переменные по‑разному. Если он видит, что перед ним число, то он даже не полезет за его адресом в кучу, а вот для объекта придется сходить все‑таки по адресу и навестить объект. Как же он это делает? Все решает последний бит. 

Как работает последний бит

Адреса объектов в памяти кратны 16. Разработчики Dart специально сделали так. Если адрес кратен 16 (10000), то значит его последние биты всегда нули, никакой информации они не несут. Вот это пустующее место и забирает Dart под флажок. Последний бит говорит, чем является значение в указателе. Если он равен 1, значит перед нами ссылка на объект, если 0, значит это число и не нужно искать объект по адресу.

Рассмотрим конкретный пример на произвольном адресе:
Допустим у нас объект лежит по адресу 0x00A03F50 — этот адрес кратен 16.
В двоичном виде последний байт этого адреса выглядит так: 0101 0000.
Видим на конце нули, а зная то, что у нас на конце в адресе всегда будут нули, то это можно использовать для своих нужд. В данном случае Dart сделает это так 0101 0001->0x00A03F51 — поставит единицу в конце, т.е пометит, что это адрес объекта.

Хорошо, теперь обратное действие, нам надо дойти до реального объекта.
Убираем нашу единицу 0x00A03F51->0x00A03F50 = 0101 0000.

Именно тут очень важно, чтобы адрес был кратен 16, ведь если число будет меньше, то и свободных битов будет на конце меньше, а если будем брать кратность 32, то это слишком большое потребление по памяти и на мелких объектах память тратилась бы впустую. Но вот про smi(small integer) числа все немного интереснее.

Smi: число прямо в указателе


Если число уместилось в указателе, то его можно назвать smi (small integer).
Smi не занимает места в памяти напрямую, живет прямо в указателе, а значит искать его по адресу и убирать за ним не нужно. Однако сам указатель занимает 8 байт, поэтому можно считать, что любое smi занимает 8 байт.

Возьмем пример, число 7 = 111 (в битах)
Когда Dart упаковывает 7 в Smi, он сдвигает биты влево на 1 позицию. Сдвиг влево это умножение на два. Получаем 1110. Ноль на конце означает, что наш указатель будет распаковывать в будущем это значение как число smi, поделив число на 2. (1110 / 2 = 111). 

Handles: зачем нужен мостик для C++


Важная деталь. Сборщик мусора периодически двигает объекты, чтобы бороться с фрагментацией и ускорить работу с памятью. Объект переезжает на новое место и приобретает новый адрес. Для кода на Dart это не является проблемой. Сборщик и так знает все ссылки внутри Dart и сам их обновляет на новые.

Но вот что делать с кодом на C++, это сам движок и нативные библиотеки. Сборщик понятия не имеет где у этого кода лежат ссылки. Если объект переедет, C++ останется со старым адресом, и программа упадёт.
Для решения этой проблемы придумали handle. Handle это ссылка на ссылку. Код на C++ держит не сам объект, а handle с адресом объекта. Когда объект переезжает, то сборщик обновляет адрес внутри handle. А так как C++ всегда смотрит на ссылку, которая ссылается на ссылку, то мы исключаем проблемы с пропажей адреса.

Что же из первой части может быть полезно на практике


1. Числа почти бесплатны. Int: индексы, счетчики и т.д все это smi. В память ничего из этого не попадает, сборщик за ними не следит.
2. Любой новый созданный объект — это работа для сборщика. Чем больше бездумных объектов мы создаем, тем больше давим на память и работу сборщика.
В следующей части посмотрим, почему сборщик делит память на два поколения и почему временные объекты в Dart обходятся так дёшево.

Источники

— Vyacheslav Egorov. Introduction to Dart VM, раздел Garbage Collection: https://mrale.ph/dartvm/gc.html‑ Исходный код Dart SDK (runtime/vm/heap): scavenger, marker, sweeper, compactor.

Комментарии (3)


  1. AndreyDmitriev
    01.07.2026 10:42

    У меня такой чисто дилетантский вопрос, если позволите. В чём вообще преимущество Дарта? Я это к тому, что хотя я в вебе не очень уверенно, но я веду чисто для себя записную книжку на Hugo, и шорткоды, которые на гитхабе нарыл, они как раз Dart Sass используют, в основном модульность. Но это доп звисимость, и мне пришлось повозиться, чтобы всё это запустить, и сделать деплой на гитхабе но оно работает. Однако там же довольно несложный процессинг-"транспиляция" из sass в css, ведь тоже самое можно было бы сделать и на Го и на Расте? В чём именно Дарт тут хорош? Я просто для расширения кругозора интересуюсь.


    1. MiT_73
      01.07.2026 10:42

      Не совсем знаком с Sass и тем более с реализацией Dart Sass, но исходя из своего опыта могу сказать почему:

      1. Очень легко сделать cli под любую систему, не нужно думать об архитектуре сборки как в c++/rust. Не нужно думать об виртуальной машине в системе, как для Java/Kotlin

      2. Имеет встроенный транслятор под js, он дает максимально оптимизированный и чистый от сторонних библиотек код на js

      3. Имеет прямой доступ из dart к js через interop

      4. Google активно принимает участи в разработке Sass

      5. Достаточно быстро работает и имеет простой синтаксис


    1. dslmnvv Автор
      01.07.2026 10:42

      Отвечу своими словами, а в конце дам ссылку на официальные заявления.

      Как разработчики, мы любим поменьше зависимостей, поменьше возни с разными версиями и вообще любим, когда все автоматизировано. И вот тут Dart получается как двойной агент: из коробки он и в бинарник собирается, и в JS компилируется. Для Sass это как раз очень важно, поэтому ставку и сделали на Dart Sass.

      Еще, насколько я понимаю, сыграла роль близость к языку: ведущий разработчик Sass работает в Google, где Dart и сделали, так что он был под рукой и хорошо знаком.

      Ну и честно скажу, я, как и вы, не силен в вебе, поэтому просто оставлю ссылку на первоисточник, где команда Sass сама все объясняет: https://sass-lang.com/blog/announcing-dart-sass/