Как сборщик мусора 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 справляется с этим? (или это просто утечка памяти?)

8 ответов


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

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


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

How?

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

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

  1. локальные переменные в методе Main
  2. основной поток
  3. статические переменные main класс!--9-->

enter image description here

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

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

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

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

enter image description here

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


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

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

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

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


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

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

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

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

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

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

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


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

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

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

TofuBeer был более подробно, если хочешь.

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

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


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

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


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