Сборщик мусора в Dart: поколения и scavenge — полное руководство
Каждый разработчик, работающий с Dart или Flutter, рано или поздно задаётся вопросом: почему приложение иногда подлагивает, а память расходуется неэффективно? Ответ часто кроется в том, как язык управляет памятью. В этой статье мы подробно разберём, как устроен сборщик мусора в Dart, почему куча раз

Каждый разработчик, работающий с Dart или Flutter, рано или поздно задаётся вопросом: почему приложение иногда подлагивает, а память расходуется неэффективно? Ответ часто кроется в том, как язык управляет памятью. В этой статье мы подробно разберём, как устроен сборщик мусора в Dart, почему куча разделена на поколения, как работает механизм scavenge и какие практические выводы из этого следует для вашего кода.
Почему Dart делит кучу на поколения
В первой части цикла мы уже разобрались, как Dart отличает число от объекта. Теперь пришло время ответить на главный вопрос: как сборщик мусора убирает за собой? Ответ кроется в наблюдении за жизненным циклом объектов в типичном приложении.
Представьте, что происходит в обычном методе build() у Flutter-виджета. За один кадр создаётся огромное количество временных объектов: EdgeInsets, Padding, промежуточные TextStyle, списки, замыкания. Эти объекты живут лишь до следующего ребилда, а затем становятся мусором. С другой стороны, есть объекты, которые живут долго: синглтоны, кэши, репозитории, состояние приложения. Они создаются один раз и существуют всё время работы программы.
Отсюда напрашивается вывод: если молодые объекты пересоздаются пачками, а старые в основном живут, то убирать их одинаково невыгодно. Именно поэтому Dart делит кучу на два поколения: молодое (new space) и старое (old space). Молодое поколение предназначено для быстро умирающих объектов, а старое — для долгоживущих. Такое разделение позволяет сборщику мусора работать эффективнее, адаптируя стратегию очистки под конкретный тип объектов.
Какие объекты попадают в молодое поколение?
В молодое поколение попадают все новые объекты, создаваемые во время выполнения программы. Это могут быть как временные объекты в методах, так и объекты, которые впоследствии станут долгоживущими. Сборщик мусора не знает заранее, сколько проживёт объект, поэтому все новые объекты изначально размещаются в молодом поколении. Если объект переживает несколько циклов сборки, он продвигается в старое поколение. Такая стратегия основана на эмпирическом наблюдении, что большинство объектов умирают молодыми.
Как работает scavenge-сборка
Scavenge — это алгоритм сборки мусора, применяемый к молодому поколению. Он основан на принципе «скопировать и очистить» (copying collection). Идея проста: молодое поколение разделено на два полупространства (from-space и to-space). Объекты создаются в from-space. Когда from-space заполняется, сборщик запускает scavenge: проходит по корням (глобальные переменные, стек, ссылки из старого поколения) и копирует все живые объекты в to-space, попутно обновляя ссылки. После этого from-space считается пустым, а полупространства меняются местами.
Ключевая особенность scavenge в том, что он работает только с молодым поколением. Объекты, которые пережили несколько циклов сборки (обычно два), повышаются до старого поколения. Это называется продвижением (promotion). Старое поколение очищается реже, с помощью другого алгоритма — mark-sweep или mark-compact, который мы рассмотрим в следующей части.
Чем scavenge отличается от обычной сборки мусора
Многие разработчики, знакомые с Java или C, привыкли, что сборщик мусора может останавливать выполнение программы на заметное время (stop-the-world). В Dart scavenge старается минимизировать паузы. Во-первых, он работает только с молодым поколением, которое обычно невелико. Во-вторых, алгоритм копирования очень быстрый: он просто перемещает живые объекты, не тратя время на сложный анализ.
Однако у scavenge есть и недостатки. Копирование объектов требует дополнительной памяти — to-space должно быть достаточно большим, чтобы вместить все живые объекты. Кроме того, если в молодом поколении много долгоживущих объектов, они будут копироваться несколько раз, что неэффективно. Именно поэтому существует механизм продвижения: объекты, пережившие несколько циклов, отправляются в старое поколение, где их не трогают до следующей серьёзной сборки.
Технические детали реализации
В Dart молодое поколение устроено как два полупространства одинакового размера. Размер по умолчанию зависит от платформы и может настраиваться через флаги виртуальной машины. Например, в командной строке можно указать --newgensemimaxsize, чтобы изменить максимальный размер полупространства. Это полезно для приложений с особыми требованиями к памяти.
Сборка мусора в молодом поколении запускается, когда from-space заполняется. В этот момент виртуальная машина останавливает все потоки (stop-the-world), чтобы безопасно скопировать объекты. Паузы обычно короткие — порядка нескольких миллисекунд, но в больших приложениях они могут стать заметными. Flutter-разработчикам важно знать, что интенсивное создание временных объектов в build-методах может приводить к частым scavenge-сборкам и, как следствие, к дропам кадров.
Ещё одна деталь: Dart использует барьеры записи (write barriers), чтобы отслеживать ссылки из старого поколения на молодое. Когда объект в старом поколении начинает ссылаться на объект в молодом, это фиксируется в специальной карте. Благодаря этому сборщик мусора знает, какие корни нужно проверять при scavenge, и не сканирует всё старое поколение.
Как настроить размер молодого поколения в Dart?
Размер полупространств можно регулировать через флаги командной строки виртуальной машины Dart. Например, --newgensemimaxsize устанавливает максимальный размер каждого полупространства в мегабайтах. Это может быть полезно для приложений, которые создают много временных объектов, но имеют ограниченную память. Однако изменение этих параметров требует тщательного тестирования, так как слишком маленький размер приведёт к частым сборкам, а слишком большой — к неэффективному использованию памяти.
Кого затронет и как
Эта информация в первую очередь важна для Flutter-разработчиков. Если ваше приложение тормозит или «лагует», возможно, дело в частых сборках мусора. Чтобы уменьшить их количество, старайтесь избегать создания лишних объектов в build-методах: выносите неизменяемые виджеты в const, используйте переиспользуемые объекты, избегайте замыканий в горячих путях.
Разработчики, пишущие на Dart вне Flutter, например, серверные приложения, тоже должны учитывать особенности сборщика. Высокая нагрузка на память может приводить к длительным паузам, что критично для систем реального времени. В таких случаях стоит профилировать приложение и настраивать параметры виртуальной машины.
Для бизнеса и пользователей это означает, что приложения, написанные с учётом особенностей сборщика мусора, будут работать плавнее и быстрее. Особенно это заметно на мобильных устройствах с ограниченной памятью.
Как оптимизировать код для снижения нагрузки на сборщик мусора?
Один из главных советов — использовать const-конструкторы для виджетов, которые не изменяются. Это позволяет Dart создавать их только один раз и переиспользовать. Также стоит избегать создания новых объектов в циклах и горячих путях, например, в методе build. Вместо этого можно выносить неизменяемые данные в статические поля или использовать кэширование. Ещё один приём — использовать списки фиксированной длины, если размер известен заранее, чтобы избежать лишних аллокаций.
Что будет дальше
В следующей части мы рассмотрим сборку мусора в старом поколении: алгоритмы mark-sweep и mark-compact, а также то, как Dart борется с фрагментацией памяти. Также стоит ожидать, что команда Dart продолжит оптимизировать сборщик мусора для новых платформ, таких как WebAssembly и встраиваемые устройства.
Уже сейчас можно сказать, что понимание работы сборщика мусора — это не просто академический интерес. Это практический инструмент для создания высокопроизводительных приложений. Следите за новыми статьями цикла, чтобы углубить свои знания.
Итог
Разделение кучи на поколения и использование scavenge-алгоритма позволяют Dart эффективно управлять памятью, минимизируя паузы и снижая нагрузку на приложение. Понимание этих механизмов помогает разработчикам писать более производительный код и избегать типичных ошибок, связанных с созданием лишних объектов. Если вы хотите, чтобы ваши Flutter-приложения работали без лагов, начните с оптимизации создания объектов в горячих путях — и сборщик мусора скажет вам спасибо.