Как работает сборка мусора Java с циркулярными ссылками?

Из моего понимания, сборка мусора в Java очищает некоторый объект, если ничто иное не указывает на этот объект.

Мой вопрос: что произойдет, если у нас есть что-то вроде этого:

class Node {
    public object value;
    public Node next;
    public Node(object o, Node n) { value = 0; next = n;}
}

//...some code
{
    Node a = new Node("a", null), 
         b = new Node("b", a), 
         c = new Node("c", b);
    a.next = c;
} //end of scope
//...other code

a, b и c должны быть собраны мусор, но все они ссылаются на другие объекты.

Как с этим связано сборка мусора Java? (или это просто утечка памяти?)

Ответ 1

Java GC рассматривает объекты "мусор", если они недоступны через цепочку, начинающуюся с корня коллекции мусора, поэтому эти объекты будут собраны. Даже если объекты могут указывать друг на друга, чтобы сформировать цикл, они все еще мусор, если они отрезаны от корня.

См. раздел о недоступных объектах в Приложении A: "Правда об сборке мусора" в Производительность платформы Java: стратегии и тактика для подробностей.

Ответ 2

yes Java Сборщик мусора обрабатывает круговую ссылку!

How?

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

Простое приложение Java имеет следующие корни GC:

  • Локальные переменные в основном методе
  • Основной поток
  • Статические переменные основного класса

enter image description here

Чтобы определить, какие объекты больше не используются, JVM периодически запускает то, что очень точно называют алгоритмом mark-and-sweep. Он работает следующим образом:

  • Алгоритм проходит все ссылки на объекты, начиная с GC корней и отмечает каждый объект, найденный как живой.
  • Вся память кучи, которая не занята отмеченными объектами, утилизирован. Он просто обозначен как свободный, по сути, неиспользуемые объекты.

Поэтому, если какой-либо объект недоступен из корней GC (даже если он является саморегументированным или циклическим), он будет подвергнут сбору мусора.

Иногда это может привести к утечке памяти, если программист забыл разыменовать объект.

enter image description here

Источник: Управление памятью Java

Ответ 3

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

Есть, конечно, немало вариаций, в основном ради скорости. Например, большинство современных сборщиков мусора являются "поколением", что означает, что они делят объекты на поколения, а по мере того, как объект становится старше, сборщик мусора идет дольше и дольше между временами, когда он пытается выяснить, остается ли этот объект действительным или нет - он только начинает предполагать, что если он прожил долгое время, шансы довольно хорошие, что он будет продолжать жить еще дольше.

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

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

Ответ 4

Вы правы. Конкретная форма сбора мусора, которую вы описываете, называется "подсчет ссылок". То, как это работает (концептуально, по крайней мере, большинство современных реализаций подсчета ссылок фактически реализовано совсем по-другому) в простейшем случае выглядит следующим образом:

  • всякий раз, когда добавляется ссылка на объект (например, она назначается переменной или полем, передается методу и т.д.), счетчик ссылок увеличивается на 1
  • всякий раз, когда ссылка на объект удаляется (метод возвращается, переменная выходит за пределы области, поле повторно назначается другому объекту или объект, который содержит поле, получает сам мусор, собранный), счетчик ссылок уменьшено на 1
  • как только число ссылок достигает 0, больше нет ссылки на объект, что означает, что никто больше не может его использовать, поэтому он является мусором и может быть собрано

И эта простая стратегия имеет именно ту проблему, которую вы определяете: если A ссылается на B и B, ссылается на A, то оба их подсчета ссылок никогда не могут быть меньше 1, что означает, что они никогда не будут собраны.

Существует четыре способа решения этой проблемы:

  • Игнорируйте его. Если у вас достаточно памяти, ваши циклы малы и нечасты, а время выполнения короткое, возможно, вам удастся просто не собирать циклы. Подумайте о интерпретаторе оболочки script: сценарии оболочки обычно запускаются всего несколько секунд и не выделяют много памяти.
  • Объедините свой счетный сборщик мусора с другим сборщиком мусора, который не имеет проблем с циклами. CPython делает это, например: главный сборщик мусора в CPython является сборщиком подсчета ссылок, но время от времени трассировочный сборщик мусора запускается для сбора циклов.
  • Обнаружение циклов. К сожалению, обнаружение циклов на графике является довольно дорогостоящей операцией. В частности, для этого требуется почти то же самое накладное расходы, что и коллекционер трассировки, поэтому вы могли бы использовать один из них.
  • Не реализуйте алгоритм наивным образом, как вы и я: с 1970-х годов было разработано несколько довольно интересных алгоритмов, которые объединяют обнаружение цикла и подсчет ссылок за одну операцию умным способом, который значительно дешевле, чем либо делать их как отдельно, либо делать трассировочный сборщик.

Кстати, другой важный способ реализовать сборщик мусора (и я уже намекнул на это пару раз выше), отслеживает. Трассировочный сборник основан на концепции достижимости. Вы начинаете с некоторого набора корней, который, как вы знаете, всегда доступен (глобальные константы, например, или класс Object, текущая лексическая область, текущий стек кадров), и оттуда вы отслеживаете все объекты, доступные из корня set, затем все объекты, которые достижимы из объектов, доступных из набора корней и т.д., до тех пор, пока у вас не будет транзитивного закрытия. Все, что не в этом закрытии, - это мусор.

Поскольку цикл доступен только внутри себя, но недоступен из набора корней, он будет собран.

Ответ 5

Java-GC на самом деле не ведут себя так, как вы описываете. Точнее сказать, что они начинаются с базового набора объектов, часто называемых "GC-корнями", и будут собирать любой объект, который не может быть достигнут из корня.
Корни GC включают такие вещи, как:

  • статические переменные
  • локальные переменные (включая все применимые ссылки 'this') в настоящее время в стеке работающего потока

Итак, в вашем случае, когда локальные переменные a, b и c выходят за пределы области действия в конце вашего метода, больше нет корней GC, которые прямо или косвенно содержат ссылку на любой из ваших трех узлов, и они будут иметь право на сбор мусора.

Ссылка TofuBeer содержит более подробную информацию, если вы этого хотите.

Ответ 6

В этой статье подробно рассматривается сборщик мусора (концептуально... существует несколько реализаций). Соответствующая часть вашего сообщения - "A.3.4 Недостижимый".

Ответ 7

Сбор мусора обычно не означает "очистить некоторый объект, если ничто иное не указывает на этот объект" (этот подсчет ссылок). Сбор мусора грубо означает поиск объектов, которые не могут быть достигнуты из программы.

Итак, в вашем примере после того, как a, b и c выходят из области видимости, они могут быть собраны GC, так как вы больше не можете обращаться к этим объектам.

Ответ 8

Билл ответил на ваш вопрос напрямую. Как сказал Амнон, ваше определение сбора мусора - это просто подсчет ссылок. Я просто хотел добавить, что даже очень простые алгоритмы, такие как сборка меток и подметания и копирования, легко обрабатывают циклические ссылки. Итак, ничего волшебного в этом нет!