Перейти к основному содержимому

Сборка мусора и циклические ссылки в Java

· 4 мин. чтения

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 станут недостижимыми для остальной части графа объектов, их счетчик ссылок никогда не достигнет нуля. Это потому, что они все еще содержат ссылку друг на друга.

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

./1d656a1054d15cdf2681cc61f377be47.png

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

./ee4be0b844e639f4f58b1a4681101a0d.png

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

3. Отслеживание GC

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

./db501a5f6277649f42d8ef9c25d959fd.png

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

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

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

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

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

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

3.1. JVM HotSpot

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

4. Вывод

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

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