Как работает сборка мусора?
Очередной вопрос, ответ на который нужно начинать с уточнения: в каком именно сборщике мусора? Понятие сборщика мусора вводится в спецификации JVM, но внутренности зависят от реализации. Одна JVM может содержать несколько сборщиков, один сборщик может применять разные алгоритмы в разных случаях. Вообще говоря, в теории GC может делать ничего. Метод System.gc() обещает, что сборщик сделает «лучшую попытку» освободить память, то есть по факту не дает никаких гарантий.
GC (garbage collector) – центральная тема шуток про «джава тормозит». Это необходимая плата за стабильное автоматическое управление памятью. Поэтому это одна из самых бурлящих и меняющихся областей мира Java.
Основные подходы к сборке мусора – подсчет ссылок (reference counting) и обход графа достижимых объектов (mark-and-sweep, copying collection). Первый подход испытывает трудности с циклическими ссылками, в Java в основном используется второй.
Большинство сборщиков опирается на слабую гипотезу о поколениях. Гипотеза предполагает, что молодые объекты умирают чаще. Для этого куча делится на регионы по времени жизни объектов – поколения. Сборка мусора в них выполняется раздельно.
Общий для большинства сборщиков алгоритм описан во множестве статей, например, в этой. Суть его в том, что достижимые объекты помечаются и группируются, а недостижимые удаляются.
GC Roots – то, с чего начинается обход графа объектов на вопрос достижимости. Множество корневых объектов (root set) считается достижимым безусловно. Часто на интервью просят их перечислить.
Важное понятие для сборщиков мусора – Stop The World пауза. Это полная остановка потоков программы для безопасной сборки мусора и других системных операций. Происходит в специальных местах программы, которые называются safepoint.
Конкретный сборщик в HotSpot указывается в параметре запуска JVM. Каждый сборщик имеет много специфичных для него настроек. В Java 10 HotSpot доступно 4 сборщика:
Serial – однопоточный, с поколениями. Дает большой throughput (маленькая сумма задержек);
Parallel – многопоточный вариант Serial;
CMS (Concurrent Mark-Sweep) – дает меньшую latency (маленькие отдельные паузы), выполняя часть сборки вне Stop The World. Плата за это – меньший throughput. Способ сборки примерно как в предыдущих, работает с поколениями. В Java 9 уже объявлен deprecated;
G1 (Garbage First) – тоже направлен на уменьшение latency. Вместо поколений оперирует регионами;
Скоро будет добавлен новый сборщик Shenandoah;
Настоятельно рекомендуется к изучению очередной доклад Шипилёва (с продолжением) и цикл статей на хабре.
Сборщик мусора Garbage Collection
Чтобы понять, как работает сборщик мусора Garbage Collection, необходимо иметь представление о распределении памяти в JVM (Java Virtual Machine). Данная статья не претендует на то, чтобы покрыть весь объем знаний о распределении памяти в JVM и описании Garbage Collection, поскольку он слишком огромен. Да, к тому же, об этом достаточно информации уже имеется в Сети, чтобы желающие могли докапаться до ее глубин. Но, думаю, данной статьи будет достаточно, чтобы иметь представление о том, как JVM работает с памятью java-приложения.
Респределение памяти в JVM
Для рассмотрения вопроса распределения памяти JVM будем использовать широко распространенную виртуальную машину для Windows от Oracle HotSpot JVM (раньше был от Sun). Другие виртуальные машины (из комплекта WebLogic или open source JVM из Linux) работают с памятью по похожей на HotSpot схеме. Возможности адресации памяти, предоставляемые архитектурой ОС, зависят от разрядности процессора, определяющего общий диапазон емкости памяти. Так, например, 32-х разрядный процессор обеспечивает диапазон адресации 2 32 , то есть 4 ГБ. Диапазон адресации для 64-разрядного процессора (2 64 ) составляет 16 экзабайт.
Разделение памяти JVM
Память процесса делится на Stack (стек) и Heap (куча) и включает 5 областей :
- Stack
- Permanent Generation — используемая JVM память для хранения метаинформации; классы, методы и т.п.
- Code Cache — используемая JVM память при включенной JIT-компиляции; в этой области памяти кешируется скомпилированный платформенно-зависимый код.
- Eden Space — в этой области выделяется память под все создаваемые программой объекты. Жизненный цикл большей части объектов, к которым относятся итераторы, объекты внутри методов и т.п., недолгий.
- Survivor Space — здесь хранятся перемещенные из Eden Space объекты после первой сборки мусора. Объекты, пережившие несколько сборок мусора, перемещаются в следующую сборку Tenured Generation.
- Tenured Generation хранит долгоживущие объекты. Когда данная область памяти заполняется, выполняется полная сборка мусора (full, major collection).
Permanent Generation
Область памяти Permanent Generation используется виртуальной машиной JVM для хранения необходимых для управления программой данных, в том числе метаданные о созданных объектах. При каждом создании объекта JVM будет сохранять некоторый набор данных об объекте в области Permanent Generation. Соответственно, чем больше создается в программе объектов, тем больше требуется «пространства» в Permanent Generation.
Размер Permanent Generation можно задать двумя параметрами виртуальной машины JVM :
- -XX:PermSize – минимальный размер выделяемой памяти для Permanent Generation;
- -XX:MaxPermSize – максимальный размер выделяемой памяти для Permanent Generation.
Для «больших» Java-приложений можно при запуске определить одинаковые значения данных параметров, чтобы Permanent Generation была создана с максимальным размером. Это может увеличить производительность, поскольку динамическое изменение размера Permanent Generation является «дорогостоящей» (трудоёмкой) операцией. Определение одинаковых значений этих параметров может избавить JVM от выполнения дополнительных операций, связанных с проверкой необходимости изменения размера Permanent Generation.
Область памяти Heap
Куча Heap является основным сегментом памяти, где хранятся создаваемые объекты. Heap делится на два подсегмента : Tenured (Old) Generation и New Generation. New Generation в свою очередь делится на Eden Space и Survivor.
При создании нового объекта, когда используется оператор ‘new’, например byte[] data = new byte[1024], этот объект создаётся в сегменте Eden Space. Кроме, собственно данных для массива байт, создается также ссылка (указатель) на эти данные. Если места в сегменте Eden Space уже нет, то JVM выполняет сборку мусора. При сборке мусора объекты, на которые имеются ссылки, не удаляются, а перемещаются из одной области в другую. Так, объекты со ссылками перемещаются из Eden Space в Survivor Space, а объекты без ссылок удаляются.
Если количество используемой Eden Space памяти превышает некоторый заданный объем, то Garbage Collection может выполнить быструю (minor collection) сборку мусора. По сравнению с полной сборкой мусора данный процесс занимает немного времени, и затрагивает только область Eden Space — устаревшие объекты без ссылок удаляются, а выжившие перемещаются в область Survivor Space.
Размер сегмента Heap можно определить двумя параметрами : Xms (минимум) и -Xmx (максимум).
В чем отличие между сегментами Stack и Heap?
- Heap (куча) используется всеми частями приложения, а Stack используется только одним потоком исполнения программы.
- Новый объект создается в Heap, а в памяти Stack’a размещается ссылка на него. В памяти стека также размещаются локальные переменные примитивных типов.
- Объекты в куче доступны из любого места программы, в то время, как стековая память не доступна для других потоков.
- Если память стека полностью занята, то Java Runtime вызывает исключение java.lang.StackOverflowError, а если память кучи заполнена, то вызывается исключение java.lang.OutOfMemoryError: Java Heap Space.
- Размер памяти стека, как правило, намного меньше памяти в куче. Из-за простоты распределения памяти (LIFO), стековая память работает намного быстрее кучи.
Garbage Collector
Сборщик мусора Garbage Collector выполняет всего две задачи, связанные с поиском мусора и его очисткой. Для обнаружения мусора существует два подхода :
- Reference counting – учет ссылок;
- Tracing – трассировка.
Reference counting
Суть подхода «Reference counting» связана с тем, что каждый объект имеет счетчик, который хранит информацию о количестве указывающих на него ссылок. При уничтожении ссылки счетчик уменьшается. При нулевом значении счетчика объект можно считать мусором.
Главным недостатком данного подхода является сложность обеспечения точности счетчика и «невозможность» выявлять циклические зависимости. Так, например, два объекта могут ссылаться друг на друга, но ни на один из них нет внешней ссылки. Это сопровождается утечками памяти. В этой связи данный подход не получил распространения.
Tracing
Главная идея «Tracing» связана с тем, что до «живого» объекта можно добраться из корневых точек (GC Root). Всё, что доступно из «живого» объекта, также является «живым». Если представить все объекты и ссылки между ними как дерево, то необходимо пройти от корневых узлов GC Roots по всем узлам. При этом узлы, до которых нельзя добраться, являются мусором.
Данный подход, обеспечивающий выявление циклических ссылок, используется в виртуальной машине HotSpot VM. Теперь, осталось понять, а что представляет из себя корневая точка (GC Root)? «Источники» говорят, что существуют следующие типы корневых точек :
- Основной Java поток.
- Локальные переменные в основном методе.
- Статические переменные основного класса.
Таким образом, простое java-приложение будет иметь следующие корневые точки:
- Параметры main метода и локальные переменные внутри main метода.
- Поток, который выполняет main.
- Статические переменные основного класса, внутри которого находится main метод.
Очистка памяти
Имеется несколько подходов к очистке памяти, которые в совокупности определяют принцип функционирования Garbage Collection.
Copying collectors
При использовании «Copying collectors» область памяти делится на две части : в одной части размещаются объекты, а вторая часть остается чистой. На время очистки мусора приложение останавливает работу и запускается сборщик мусора, который находит в первой области объекты со ссылками и переносит их во вторую (чистую) область. После этого, первая область очищается от оставшихся там объектов без ссылок, и области меняются местами.
Главным достоинством данного подхода является плотное заполнение памяти. Недостатком «Copying collectors» является необходимость остановки приложения и размеры двух частей памяти должны быть одинаковыми на случай, когда все объекты остаются «живыми».
Данный подход в чистом виде в HotSpot VM не используется.
Mark-and-sweep
При использовании «mark-and-sweep» все объекты размещаются в одном сегменте памяти. Сборка мусора также приостанавливает приложение, и Garbage Collection проходит по дереву объектов, помечая занятые ими области памяти, как «живые». После этого, все не помеченные участки памяти сохраняются в «free list», в которой будут, после завершения сборки мусора, размещаться новые объекты.
К недостаткам данного подхода следует отнести необходимость приостановки приложения. Кроме этого, время сборки мусора, как и время приостановки приложения, зависит от размера памяти. Память становится «решетчатой», и, если не применить «уплотнение», то память будет использоваться неэффективно.
Данный подход также в чистом виде в HotSpot VM не используется.
Generational Garbage Collection
JVM HotSpot использует алгоритм сборки мусора типа «Generational Garbage Collection», который позволяет применять разные модули для разных этапов сборки мусора. Всего в HotSpot реализовано четыре сборщика мусора :
- Serial Garbage Collection
- Parallel Garbage Collection
- CMS Garbage Collection
- G1 Garbage Collection
Serial Garbage Collection относится к одним из первых сборщиков мусора в HotSpot VM. Во время работы этого сборщика приложение приостанавливается и возобновляет работу только после прекращения сборки мусора. В Serial Garbage Collection область памяти делится на две части («young generation» и «old generation»), для которых выполняются два типа сборки мусора :
- minor GC – частый и быстрый c областью памяти «young generation»;
- mark-sweep-compact – редкий и более длительный c областью памяти «old generation».
Область памяти «young generation», представленная на следующем рисунке, разделена на две части, одна из которых Survior также разделена на 2 части (From, To).

Алгоритм работы minor GC
Алгоритм работы minor GC очень похож на описанный выше «Copying collectors». Отличие связано с дополнительным использованием области памяти «Eden». Очистка мусора выполняется в несколько шагов :
- приложение приостанавливается на начало сборки мусора;
- «живые» объекты из Eden перемещаются в область памяти «To»;
- «живые» объекты из «From» перемещаются в «To» или в «old generation», если они достаточно «старые»;
- Eden и «From» очищаются от мусора;
- «To» и «From» меняются местами;
- приложение возобновляет работу.

В результате сборки мусора картинка области памяти изменится и будет выглядеть следующим образом :

Некоторые объекты, пережившие несколько сборок мусора в области From, переносятся в «old generation». Следует, также отметить, что и «большие живые» объекты могут также сразу же пеместиться из области Eden в «old generation» (на картинке не показаны).
Алгоритм работы mark-sweep-compact
Алгоритм «mark-sweep-compact» связяан с очисткой и уплотнением области памяти «old generation».

Принцип работы «mark-sweep-compact» похож на описанный выше «Mark-and-sweep», но добавляется процедура «уплотнения», позволяющая более эффективно использовать память. В процедуре живые объекты перемещаются в начало. Таким образом, мусор остается в конце памяти.
При работе с областью памяти используется механизм «bump-the-pointer», определяющий указатель на начало свободной памяти, в которой размещается создаваемый объект, после чего указатель смещается. В многопоточном приложении используется механизм TLAB (Thread-Local Allocation Buffers), который для каждого потока выделяет определенную область памяти.
Краткий конспект реализации Garbage Collector в Java
Garbage Collector (GB) часть JVM, который призван очищать память, выделенную приложению. Он должен:
- найти мусор (неиспользуемые объекты)
- удалить мусор
Есть различные реализации GB.
Поиск мусора
- Reference counting — у каждого объекта счетчик ссылок. Когда он равен нулю, объект считается мусором. Проблема такого подхода в том, что могут быть цикличные ссылки у объектов друг на друга, в то время как они фактически мусор и не используются программой.
- Tracing — объект считается не мусором, если до него можно добраться с корневых точек (GC Root: локальные переменные и параметры методов, java-потоки, статичные переменные, ссылки из JNI.
Организация памяти JVM
Делится на две части:
- Heap — куча. Основной сегмент памяти, где содержатся все объекты и происходит сборка мусора.
- Permanent Generation — содержит мета-данные классов.
Сразу про Permanent Generation. Может менять размер во время выполнения, и это довольно дорогостоящая операция. Размер настраивается (-XX: PermSize — мин размер, -XX: MaxSize — макс размер). Часто мин = макс.
Heap. Куча. Тут и работает GC.
Делится на две области:
- New (Yang) Generation — объекты, кот. тут считаются короткоживущими.
- Old Generation (Tenured) — обекты считаются долгоживущими.
Алгоритм GC исходит из того предположения, что большинство java-объектов живут недолго. Быстро становятся мусором. От них необходимо довольно оперативно избавляться. Что и происходит в New Generation. Там сбор мусора гораздо чаще, чем в Old Generation, где хранятся долгоживущие объекты. После создания объект попадает в New Generation и имеет шанс попасть в Old Generation по прошествии некоторого времени (циклов GC).
Heap состоит из:
- Eden — переводится как Едем (?). Сюда аллоцируются объекты. Если нет места запускается GC.
- Survivor — точнее их два, S1 и S2, и они меняются ролями. Хранятся объекты, которые признаются живыми во время GC.
Размер Heap настраивается.
Принцип работы 4 сборщиков HotSpot VM (одна из JVM)
- Serial
- Parallel
- Concurent Mark Sweep (CMS)
- Garbage-First (G1)
Serial. Когда нет места в Eden, запускается GC, живые объекты коприруются в S1. Вся область Eden очищается. S1 и S2 меняются местами. При последующих циклах в S1 будут записаны живые объекты как из Eden, так и из S2. После нескольких циклов обмена S1 и S2 или заполнения области S2, обекты, которые живут достаточно долго перемещаются в Old Greneration.
Следует сказать, что не всегда объекты при создании аллоцируюся в Eden. Если объект слишком велик, он сразу идет в Old Generation.
Когда после очередной сборки мусора места нехватает уже в New Generation, то запускается сбор мусора в Old Generation (наряду со сборкой New Generation). В old Generation объекты уплотняются (алгоритм Mark-Sweep-Compact).
Если после полной сборки мусора места нехватает, то вылетает Java.lang.OutOfMemoryError.
Но во время работы VW может запрашивать увеличение памяти и Heap может увеличиваться.
Как правило, Old Generation занимает 2/3 объема Heap.
Эффективоность алгоритма сборки мусора считается по параметру STW (Stop The World) — время, когда все процессы кроме GC останавливаются. Serial в этом смысле не слишком эффективен, т.к. делает свою работу не торопясь, в одном потоке.
Parallel. То же, что и Serial, но использует для работы несколько потоков. Таким образом STW чуть меньше.
Concurent Mark Sweep. Принцип работы с New Generation такой же, как и в случае алгоритмов Serial и Parallel, отличия в том, что данный алгоритм разделяет младшую (New Generation) и старшую (Old Generation) сборку мусора во времени. Причем сбор мусора в Old Generation происходит в отдельном потоке, независимо от младшей сборки. При этом сначала приложение останавливается, сборщик помечает все живые объекты доступные из GC Root (корневых точек) напрямую, затем приложение вновь начинает работу, а сбощик проверяет объекты доступные по ссылкам из этих самых помеченных, и также помечает их как живые. Эта особенность создает так называемые плавающие объекты, которые помечены как живые, но таковыми по факту не являющимися. Но они будут удалены в следующих циклах. Т.е. пропускная способность растет, STW уменьшается, но требутся больше места для хранения плавающих объектов.
В этом алгоритме уплотнения нет. Т.е. область Old Generation дефрагментированна.
Garbage-First. G1 сильно отличается от своих предшественников. Он делит область Heap не физически, а скорее логически на те же области: Eden, Survivor, Old Generation. Причем дефрагментированно. Физически область Heap делится на регионы одинакового размера, каждый из которых может быть Eden, Survivor или Old Generation + область для больших объектов (громадный регион).
Над очисткой регионов Eden работает сразу несколько потоков, объекты переносятся в регионы Survivor или регионы старшего поколения (Tenured). Это знакомый по предыдущим алгоритмам очистки подход. На время очистки работа приложения останавливается. Отличие в том, что очистка производится не по всем регионам Eden, а только по некоторым, которые более всего в ней нуждаются, таким образом регулируется время очистки. Отсюда название алгоритма — в первую очередь мусор.
А с полной сборкой (точнее, здесь она называется смешанной (mixed)) все немного хитроумнее, чем в рассмотренных ранее сборщиках. В G1 существует процесс, называемый циклом пометки (marking cycle), который работает параллельно с основным приложением и составляет список живых объектов. За исключением последнего пункта, этот процесс выглядит уже знакомо для нас:
- Initial mark. Пометка корней (с остановкой основного приложения) с использованием информации, полученной из малых сборок.
- Concurrent marking. Пометка всех живых объектов в куче в нескольких потоках, параллельно с работой основного приложения.
- Remark. Дополнительный поиск не учтенных ранее живых объектов (с остановкой основного приложения).
- Cleanup. Очистка вспомогательных структур учета ссылок на объекты и поиск пустых регионов, которые уже можно использовать для размещения новых объектов. Первая часть этого шага выполняется при остановленном основном приложении.
После окончания цикла пометки G1 переключается на выполнение смешанных сборок. Это значит, что при каждой сборке к набору регионов младшего поколения, подлежащих очистке, добавляется некоторое количество регионов старшего поколения. Количество таких сборок и количество очищаемых регионов старшего поколения выбирается исходя из имеющейся у сборщика статистики о предыдущих сборках таким образом, чтобы не выходить за требуемое время сборки. Как только сборщик очистил достаточно памяти, он переключается обратно в режим малых сборок.
Очередной цикл пометки и, как следствие, очередные смешанные сборки будут запущены тогда, когда заполненность кучи превысит определенный порог.
Опираясь на уже упомянутую статистику о предыдущих сборках, G1 может менять количество регионов, закрепленных за определенным поколением, для оптимизации будущих сборок.
Громадные регионы. С точки зрения JVM объекты которые превышают размер половины региона являются громадными. Особенности:
- никогда не перемещается между регионами
- может удаляться в рамках цикла пометки или полной сборки мусора
- в регионе, занятом громадным объектом, может находится только он сам.
Громадные объекты в силу небольшого размера регионов могут порождать проблемы с точки зрения STW.
G1 выигрывает по времени STW, но расплатой является меньшая пропускная способность (около 90%, ср., например у Paraller ок. 99%) т.е. большие затраты ресурсов процессора.
Bonus
Вопрос: Расскажите почему именно два региона survival и зачем перекладывать объекты между ними?
Ответ:
Представьте себя на месте сборщика. У вас есть регион памяти, который нужно очистить. После удаления мусора регион оказывается сильно дефрагментированным и если вы хотите это исправить, то у вас есть два варианта: либо уплотнять объекты в рамках этого же региона, либо скопировать их в другой, пока еще пустой регион, располагая один-к-одному, а старый регион объявить пустым. Но задача осложняется тем, что объекты ссылаются друг на друга и при перемещении любого объекта необходимо производить обновление всех имеющихся на него ссылок. И вот эту задачу намного легче решать при копировании, причем сразу объединяя ее с задачей поиска живых объектов:Вы просто заводите два указателя на начало новой области. Первый указатель (назовем его T) смещается вправо каждый раз, когда в новую область копируется объект, то есть он всегда указывает на первый свободный блок новой области. При этом на том месте старой области, где находился перемещаемый объект, мы делаем пометку о том, что он был перемещен, и там же оставляем его новый адрес. Первым делом перемещаем таким образом все руты из старой области в новую. И вот тут вступает в действие второй указатель (назовем его R). Он тоже начинает смещаться вправо по уже размещенным в новой области объектам. В каждом объекте он ищет ссылки на другие объекты и смотрит на то место в старом регионе, куда они указывают. Если там стоит метка о перемещении и новый адрес, то этот адрес используется для подмены. Если же там лежит объект, то он перемещается в новый регион, на его месте ставится метка и новый адрес, на который так же заменяется ссылка, по которой его нашли, при этом T опять смещается вправо. Как только R догонит T, окажется, что мы собрали все живые объекты в новой области, размещенные компактно, да еще и с корректно обновленными ссылками, а старый регион можем объявить пустым. Все быстро и просто.
29 06 2017
- Java
- Android Quizzes 2021
- Реактивное программирование с применением RxJava
- Android изнутри. Часть I
- Большие запросы к базе данных на Android
- ProgressDialog умер, да здравствует ProgressBar!
- Введение в Android Architecture Components
- Firebase Tutorial. Part II. — Как автоматически развернуть сайт на хостинге Firebase из GitHub-репозитория?
- Firebase Tutorial — Firebase database. Part I.
- У меня было 10 Android собеседований за последние два года. (перевод)
- Android Tutorial — Facebook SDK Login. Part III. Get Posts.
- Android Tutorial — Facebook SDK Login. Part II. Share content.
- Android Tutorial — Facebook SDK Login. Part I.
- Оптимизация прокрутки вложенных в друг друга RecyclerView
- RecyclerView и ListView. В чем разница?
- Различные типы Item View в RecyclerView
- Тесты сделают вас счастливее (перевод).
- Пример анимации в Android с использованием TimeAnimator.
- Android. Два способа записать результат Timer в пользовательский поток (UI Thread).
- Видео. Architecture Components. Solving the Lifecycle Problem. Перевод субтитров.
- Зачем нужны интерфейсы в ООП?
- Видео. Architecture Components. Improve Your App’s Design. Перевод субтитров.
- Видео. Google I/O ’17 Architecture Components — Introduction. Перевод субтитров.
- Препарирование ContentProvider
- REST. Описание концепции. Особенности реализации API. Максимально кратко.
- Лоадеры (loaders) в Android. Для чего нужны. Конспект.
- Volley vs Retrofit. Описание библиотек REST API.
- Краткий конспект реализации Garbage Collector в Java
- Препарирование RecyclerView
- Лексикон прописных истин Android
- Тесты сделают вас счастливее (перевод).
- Видео. Architecture Components. Solving the Lifecycle Problem. Перевод субтитров.
- Зачем нужны интерфейсы в ООП?
- Видео. Architecture Components. Improve Your App’s Design. Перевод субтитров.
- Видео. Google I/O ’17 Architecture Components — Introduction. Перевод субтитров.
- Краткий конспект реализации Garbage Collector в Java
- Лекция о языках программирования
- Краткий конспект реализации Garbage Collector в Java
Сборка мусора
Управление памятью в JavaScript выполняется автоматически и незаметно. Мы создаём примитивы, объекты, функции… Всё это занимает память.
Но что происходит, когда что-то больше не нужно? Как движок JavaScript обнаруживает, что пора очищать память?
Достижимость
Основной концепцией управления памятью в JavaScript является принцип достижимости.
Если упростить, то «достижимые» значения – это те, которые доступны или используются. Они гарантированно находятся в памяти.
- Существует базовое множество достижимых значений, которые не могут быть удалены. Например:
- Выполняемая в данный момент функция, её локальные переменные и параметры.
- Другие функции в текущей цепочке вложенных вызовов, их локальные переменные и параметры.
- Глобальные переменные.
- (некоторые другие внутренние значения)
Эти значения мы будем называть корнями.
В движке JavaScript есть фоновый процесс, который называется сборщиком мусора. Он отслеживает все объекты и удаляет те, которые стали недоступными.
Простой пример
Вот самый простой пример:
// в user находится ссылка на объект let user = < name: "John" >;Здесь стрелка обозначает ссылку на объект. Глобальная переменная user ссылается на объект (мы будем называть его просто «John» для краткости). В свойстве «name» объекта John хранится примитив, поэтому оно нарисовано внутри объекта.
Если перезаписать значение user , то ссылка потеряется:
user = null;Теперь объект John становится недостижимым. К нему нет доступа, на него нет ссылок. Сборщик мусора удалит эти данные и освободит память.
Две ссылки
Представим, что мы скопировали ссылку из user в admin :
// в user находится ссылка на объект let user = < name: "John" >; let admin = user;Теперь, если мы сделаем то же самое:
user = null;…то объект John всё ещё достижим через глобальную переменную admin , поэтому он находится в памяти. Если бы мы также перезаписали admin , то John был бы удалён.
Взаимосвязанные объекты
Теперь более сложный пример. Семья:
function marry(man, woman) < woman.husband = man; man.wife = woman; return < father: man, mother: woman >> let family = marry(< name: "John" >, < name: "Ann" >);Функция marry «женит» два объекта, давая им ссылки друг на друга, и возвращает новый объект, содержащий ссылки на два предыдущих.
В результате получаем такую структуру памяти:
На данный момент все объекты достижимы.
Теперь удалим две ссылки:
delete family.father; delete family.mother.husband;Недостаточно удалить только одну из этих двух ссылок, потому что все объекты останутся достижимыми.
Но если мы удалим обе, то увидим, что у объекта John больше нет входящих ссылок:
Исходящие ссылки не имеют значения. Только входящие ссылки могут сделать объект достижимым. Объект John теперь недостижим и будет удалён из памяти со всеми своими данными, которые также стали недоступны.
После сборки мусора:
Недостижимый «остров»
Вполне возможна ситуация, при которой целый «остров» взаимосвязанных объектов может стать недостижимым и удалиться из памяти.
Возьмём объект family из примера выше. А затем:
family = null;Структура в памяти теперь станет такой:
Этот пример демонстрирует, насколько важна концепция достижимости.
Объекты John и Ann всё ещё связаны, оба имеют входящие ссылки, но этого недостаточно.
Бывший объект family был отсоединён от корня, на него больше нет ссылки, поэтому весь «остров» становится недостижимым и будет удалён.
Внутренние алгоритмы
Основной алгоритм сборки мусора называется «алгоритм пометок» (от англ. «mark-and-sweep»).
Согласно этому алгоритму, сборщик мусора регулярно выполняет следующие шаги:
- Сборщик мусора «помечает» (запоминает) все корневые объекты.
- Затем он идёт по ним и «помечает» все ссылки из них.
- Затем он идёт по отмеченным объектам и отмечает их ссылки. Все посещённые объекты запоминаются, чтобы в будущем не посещать один и тот же объект дважды.
- …И так далее, пока не будут посещены все достижимые (из корней) ссылки.
- Все непомеченные объекты удаляются.
Например, пусть наша структура объектов выглядит так:
Мы ясно видим «недостижимый остров» справа. Теперь давайте посмотрим, как будет работать «алгоритм пометок» сборщика мусора.
На первом шаге помечаются корни:
Затем помечаются объекты по их ссылкам:
…А затем объекты по их ссылкам и так далее, пока это возможно:
Теперь объекты, которые не удалось посетить в процессе, считаются недостижимыми и будут удалены:
Мы также можем представить себе этот процесс как выливание огромного ведра краски из корней, которая течёт по всем ссылкам и отмечает все достижимые объекты. Затем непомеченные удаляются.
Это концепция того, как работает сборка мусора. Движки JavaScript применяют множество оптимизаций, чтобы она работала быстрее и не задерживала выполнение кода.
Вот некоторые из оптимизаций:
- Сборка по поколениям (Generational collection) – объекты делятся на два набора: «новые» и «старые». В типичном коде многие объекты имеют короткую жизнь: они появляются, выполняют свою работу и быстро умирают, так что имеет смысл отслеживать новые объекты и, если это так, быстро очищать от них память. Те, которые выживают достаточно долго, становятся «старыми» и проверяются реже.
- Инкрементальная сборка (Incremental collection) – если объектов много, и мы пытаемся обойти и пометить весь набор объектов сразу, это может занять некоторое время и привести к видимым задержкам в выполнении скрипта. Так что движок делит всё множество объектов на части, и далее очищает их одну за другой. Получается несколько небольших сборок мусора вместо одной всеобщей. Это требует дополнительного учёта для отслеживания изменений между частями, но зато получается много крошечных задержек вместо одной большой.
- Сборка в свободное время (Idle-time collection) – чтобы уменьшить возможное влияние на производительность, сборщик мусора старается работать только во время простоя процессора.
Существуют и другие способы оптимизации и разновидности алгоритмов сборки мусора. Но как бы мне ни хотелось описать их здесь, я должен воздержаться, потому что разные движки реализуют разные хитрости и методы. И, что ещё более важно, все меняется по мере развития движков, поэтому изучать тему глубоко «заранее», без реальной необходимости, вероятно, не стоит. Если, конечно, это не вопрос чистого интереса, тогда для вас будет несколько ссылок ниже.
Итого
Главное, что нужно знать:
- Сборка мусора выполняется автоматически. Мы не можем ускорить или предотвратить её.
- Объекты сохраняются в памяти, пока они достижимы.
- Если на объект есть ссылка – вовсе не факт, что он является достижимым (из корня): набор взаимосвязанных объектов может стать недоступен в целом, как мы видели в примере выше.
Современные движки реализуют разные продвинутые алгоритмы сборки мусора.
О многих из них рассказано в прекрасной книге о сборке мусора «The Garbage Collection Handbook: The Art of Automatic Memory Management» (R. Jones и др.).
Если вы знакомы с низкоуровневым программированием, то более подробная информация о сборщике мусора V8 находится в статье A tour of V8: Garbage Collection.
Также в блоге V8 время от времени публикуются статьи об изменениях в управлении памятью. Разумеется, чтобы изучить сборку мусора, вам лучше подготовиться, узнав о том как устроен движок V8 внутри в целом и почитав блог Вячеслава Егорова, одного из инженеров, разрабатывавших V8. Я говорю про «V8», потому что он лучше всего освещается в статьях в Интернете. Для других движков многие подходы схожи, но сборка мусора отличается во многих аспектах.
Глубокое понимание работы движков полезно, когда вам нужна низкоуровневая оптимизация. Было бы разумно запланировать их изучение как следующий шаг после того, как вы познакомитесь с языком.
