«1. Обзор

В этой быстрой статье мы увидим, как JVM обеспечивает сбор недостижимых, но циклических ссылок.

Сначала мы рассмотрим различные типы алгоритмов GC. После этого мы увидим, как циклические ссылки обрабатываются в JVM.

Также стоит отметить, что GC не является частью спецификации JVM и оставлен на усмотрение разработчика. Следовательно, каждая реализация JVM может иметь разные стратегии GC или вообще не иметь их.

В этой статье мы сосредоточимся на одной конкретной реализации JVM: HotSpot JVM. Мы также можем использовать термины JVM и HotSpot JVM взаимозаменяемо на протяжении всей статьи.

2. Подсчет ссылок

Подсчет ссылок Алгоритмы GC связывают счетчик ссылок с каждым объектом. Эти алгоритмы считают объект живым, пока количество ссылок на этот объект больше нуля. Обычно среда выполнения сохраняет счетчик ссылок в заголовке объекта.

В очень простой реализации каждая новая ссылка на объект должна вызывать атомарное увеличение счетчика ссылок. Точно так же каждое новое разыменование должно запускать атомарный декремент.

Язык программирования Swift использует форму подсчета ссылок для управления памятью. Кроме того, в JVM нет алгоритма GC, основанного на подсчете ссылок.

2.1. Плюсы и минусы

С другой стороны, подсчет ссылок может распределять затраты на управление памятью на протяжении всего жизненного цикла приложения, поскольку (почти) нет периодических сбоев GC. Кроме того, он потенциально может уничтожить объекты, как только их счетчик ссылок достигнет нуля, и они станут мусором.

Подсчет ссылок тоже не бесплатный обед. В простой реализации обновление счетчика ссылок может быть неэффективным, поскольку нам нужно увеличивать или уменьшать его атомарно. Немногие оптимизации могут сделать подсчет ссылок более эффективным в этом отношении, например подходы к отложенному или буферизованному подсчету ссылок.

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

Например, предположим, что объект A ссылается на объект B и наоборот. Даже если A и B станут недостижимыми для остальной части графа объектов, их счетчик ссылок никогда не достигнет нуля. Это потому, что они все еще содержат ссылку друг на друга.

Как оказалось, циклические ссылки такого рода довольно распространены в компьютерных науках. Например, рассмотрим следующий двусвязный список. Сначала другой объект имеет ссылку на список:

Reachable List

Связанный список доступен из объекта D, поэтому он не должен собираться, и счетчики ссылок выровнены с этим ожиданием. Теперь предположим, что сам объект D становится недостижимым:

Unreachable List

Несмотря на то, что связанный список теперь также недоступен, счетчик ссылок на его компоненты больше единицы. Таким образом, с этой наивной реализацией подсчета ссылок среда выполнения не будет рассматривать этот связанный список как мусор, даже если это так.

3. Трассировка GC

Сборщики трассировки будут определять достижимость объектов, отслеживая их из набора корневых объектов, известных как корни GC. Если объект доступен из корневого объекта прямо или косвенно, то он будет считаться активным. Другие недоступны и являются кандидатами на сбор:

Tracing Collector

Вот как работает простой сборщик трассировки. Начиная с корней GC, он рекурсивно обходит граф объектов до тех пор, пока не останется серых объектов для посещения. В итоге он считает все белые объекты недостижимыми и кандидатами на сбор. Это простое изображение алгоритма трехцветной маркировки.

Мы можем думать о корнях GC как об объектах, которые, как мы уверены, живы. Например, это некоторые корни GC в Java и JVM:

    Локальные переменные или что-либо, на что прямо сейчас ссылаются кадры стека. Эти переменные используются текущими выполняющимися методами, поэтому мы не хотим их собирать Живые потоки Статические переменные Классы, загруженные системным загрузчиком классов Локальные и глобальные переменные JNI

«Сборщики трассировки, в отличие от сборщиков подсчета ссылок, будут периодически выполнять процесс сбора. Таким образом, большую часть времени выделения и назначения должны работать быстро. Однако, когда GC стартует, могут возникнуть некоторые сбои.

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

Довольно интересно, что использование резервного сборщика трассировки в тандеме с подсчетом ссылок GC является одним из традиционных подходов к фиксации циклических ссылок при подсчете ссылок.

3.1. HotSpot JVM

Все реализации GC в HotSpot JVM на момент написания этой статьи являются сборщиками трассировки, включая CMS, G1 и ZGC. Таким образом, JVM не будет страдать от проблемы с циклическими ссылками. Это ключевой вывод из этой статьи!

4. Заключение

В этой быстрой статье мы увидели, как JVM обрабатывает циклические ссылки.

Для получения более подробной информации о сборке мусора настоятельно рекомендуется ознакомиться с руководством по сборке мусора.