Сборщик мусора в
Dart - тема, которая часто всплывает на собеседованиях, но найти внятное объяснение непросто. Статьи либо слишком сложные, либо уходят в дебри реализации
VM. Давайте разберем основы: как
Dart управляет памятью, что такое указатели и почему числа не влияют на производительность.
Биты, байты и указатели:
Память компьютера - это длинный ряд ячеек, каждая из которых содержит бит (0 или 1). Процессоры работают с группами битов. В 64-битных процессорах это группы по 64 бита (8 байт). Это машинное слово.
Указатель в
Dart занимает ровно одно машинное слово - 8 байт.
Указатель - это не сам адрес, а место, где этот адрес записан. Если представить адрес дома, то листок бумаги - это указатель, а то, что на нем написано - сам адрес.
Как Dart отличает число от объекта:
Dart числа и объекты хранятся по-разному. Если переменная содержит число, оно хранится прямо в указателе - никакого перехода по адресу не требуется. Если переменная содержит объект, в указателе хранится адрес этого объекта, и чтобы получить доступ к объекту, нужно перейти по этому адресу.
Как
Dart понимает, что именно лежит в указателе? Все решает последний бит.
Адреса объектов в памяти кратны 16. Это сделано специально. Если адрес кратен 16, его последние биты всегда нули. Вот это пустующее место
Dart использует как флажок.
Последний бит говорит, чем является значение в указателе:
- 1 - ссылка на объект
- 0 - число, искать объект по адресу не нужно
Пример: объект лежит по адресу 0x00A03F50. Этот адрес кратен 16. В двоичном виде последний байт: 0101 0000.
Dart ставит единицу в конце: 0101 0001 -> 0x00A03F51. Теперь это помеченный адрес объекта.
Чтобы дойти до реального объекта, нужно убрать эту единицу и получить исходный адрес.
Smi: числа, которые живут в указателе:
Если число умещается в указателе, оно называется
smi (small integer).
Smi не занимает места в куче. Оно живет прямо в указателе. Сборщик мусора за ним не следит.
Пример: число 7 = 111 в битах. Когда
Dart упаковывает его в
smi, он сдвигает биты влево на 1 позицию. Получается 1110. Ноль на конце означает, что это число, а не ссылка. При распаковке число делится на 2, возвращая исходное значение.
Handles - почему C++ не теряет объекты:
Сборщик мусора периодически двигает объекты в памяти, чтобы бороться с фрагментацией. Объект переезжает на новый адрес. Для Dart-кода это не проблема - сборщик знает все ссылки и обновляет их.
Но есть код на
C++ (движок, нативные библиотеки). Сборщик не знает, где у
C++ лежат ссылки. Если объект переедет,
C++ останется со старым адресом - программа упадет.
Решение: handles. Это ссылка на ссылку.
C++ держит не объект, а
handle с адресом объекта. Когда объект переезжает, сборщик обновляет адрес внутри
handle.
C++ всегда смотрит на актуальный адрес через
handle.
Что из этого важно на практике:
Числа практически ничего не стоят с точки зрения производительности. Индексы, счетчики, любые небольшие значения - все это
smi. Они не выделяются в куче, не создают мусора и не нагружают сборщик. Можно смело создавать сотни тысяч таких чисел без последствий.
А вот объекты - это совсем другая история. Каждый новый объект попадает в кучу, и сборщик вынужден за ним следить. Чем больше лишних объектов создается в коде, тем чаще сборщику приходится просыпаться, чтобы убрать мусор. И тем больше времени уходит на паузы, которые пользователь может заметить.
Особенно это заметно в циклах, в анимациях, в обработке больших списков. Если на каждом шаге создавать новый объект там, где можно было обойтись числом или переиспользовать существующий, нагрузка на сборщик растет, а производительность падает.
Вывод:
Dart управляет памятью хитро, но логично. Указатели хранят либо числа (smi), либо ссылки на объекты. Различие определяется последним битом. Сборщик следит за объектами в куче, но не трогает числа.
Handles решают проблему с
C++ ссылками при перемещении объектов.
На практике это значит: числа не влияют на производительность, объекты - да. Создание лишних объектов напрямую влияет на нагрузку сборщика. Понимание этих основ помогает писать более эффективный код.