Сборка мусора в Java.

Если вы пришли в Java из С/С++, то возможно вас удивит то, что объекты в Java явно уничтожать не нужно. Хотя последнее время технология сборки мусора становится все более и более распространенной. Взять хотя бы новый модный Golang.

Да, в языках вроде С/С++ вам нужно вручную следить за тем, чтобы вовремя освобождать ненужную память во избежании возможных утечек памяти(один из способов выстрелить себе в ногу, программируя на «сях»). Но вот то важное, что обязательно нужно запомнить начинающему (возможно и не только) Java-разработчику: сборка мусора не освобождает вас от необходимости думать головой. В определенных ситуациях нужно принимать во внимание, что память имеет свойство заканчиваться, а стек переполняться и для того, чтобы ваша программа для вас самих была более предсказуемой в своем поведении и написана данная статья.

«Мертвые души».

Итак, сборщик мусора(garbage collector) занимается тем, что снимает с вас немного головной боли. Вам не нужно явно уничтожать объекты в каких-нибудь методах-деструкторах. Поэтому вы просто не можете забыть сделать это — сборщик мусора сам позаботиться об очистке занимаемой объектом памяти за вас. Как это работает?

Для начала сборщику мусора нужно определить, какие объекты являются «живыми»(то есть они где-то используются), а какие — «мертвыми»(давно не нужны и их нужно удалить, чтобы очистив память).

Наверное, объект можно назвать «живым», если на него кто-то ссылается. Таким образом, при создании ссылки на объект, можно увеличивать на один значение некоторого числового поля, условно назовем его count. Теперь оно равно 1. Если создается еще одна ссылка на этот же объект, значение count снова увеличивается и равно уже 2. При присваивании одной из ссылок значения null, поле count уменьшается. Если оно становится равным нулю, то это значит, что на данный объект никто не ссылается и его можно удалить.

Данный метод носит названия метода подсчета ссылок. На практике он правда редко используется, так как вносит значительные накладные расходы(при создании объекта или разыменовании ссылки нужно каждый раз изменять значение поля count).

Как еще определить то, что объект «живой»? Можно использовать дерево объектов, где ссылки на объекты — это его узлы. Если живой объект А ссылается на узел, представляющий объект B(говорят B достижим из А), то объект B — тоже живой. Также, если объект А ссылается на B, а B нас C, то С достижим из А. Следовательно, С — тоже живой объект.

Но тут есть одна проблема. Что если в нашем графе зависимостей появятся циклические ссылки? Например, А ссылается на B, а B на A и больше на них никто не ссылается. Эти объекты просто висят в памяти. Из-за циклических зависимостей может возникнуть утечка памяти. К счастью, сборщики мусора справляются с этой проблемой.

GC Roots.

Вот я чуть выше говорил: если A — живой объект, то и B — тоже живой. Но откуда я узнал, что A — это нужный, живой объект? Что является корнем дерева, с которого мы проверяем достижимость некоторого узла дерева из другого, живого узла?

Перед тем, как продолжить чтение при необходимости прочитайте следующую статью об устройстве памяти в Java.

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

Статические члены, объекты из стека и Old Generation вместе образуют так называемый GC Roots — корневое множество объектов. Если проверяемый сборщиком мусора объект достижим из этого корневого множества, то он — не мусор и будет помечен как живой объект.

Естественно, для определения того, что мусор, а что нет, нам нужно пройтись по всему дереву объектов в куче, для чего выполнение программы нужно остановить. Такие остановки в работе сборщика мусора называются STW(Stop-The-World)-паузами и чем она меньше, тем лучше.

Итак, мы нашли способ найти в памяти живые, нужные нам объекты. Теперь их нужно удалить.

Гипотезы о поколениях.

Когда и какие объекты удалять? Стоит ли проверять одновременно и Young Generation и Old Generation(вдруг какие-то объекты отсюда тоже уже не нужны).

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

Ниже приведено наглядное представление данной гипотезы:

На оси абсцисс отложено количество выделенной памяти под объект, а на оси ординат общий размер таких объектов. Раз молодые объекты занимают в сумме гораздо больше памяти, чем долгоживущие, то имеет смысл удалять в первую очередь именно их. Отсюда и возникает логичная идея разделить весь Java heap на две части — Young Generation и Old Generation.

Все объекты аллоцируются в Young Generation и сборщик мусора периодически может выполнять проверку этих объектов, а затем их тут же удалять. Такой алгоритм сборки мусора называется mark-and-sweep. Он позволяет быстро избавляться от кучи ненужных временных объектов. Такие сборки мусора еще называются минорными(minor gc).

В то же время, те объекты, которые пережили несколько сборок мусора, перемещаются в другую область памяти — Old Generation, которая не проверяется во время минорных сборок. Эти объекты заслужили таки покой. На время копирования программа должна остановить свое выполнение. Поэтому такой алгоритм также называется stop-and-copy. Копирование может занимать длительное время, поэтому такие простои часто нежелательны. В чистом виде такой алгоритм не слишком эффективен, если работать по нему каждую сборку, однако если в Young Generation большое место занимают объекты, пережившие много сборок, имеет смысл их переместить в другой участок памяти.

Но спустя время любая память может подходить к концу, и garbage collector может запустить полную(full gc) сборку мусора, проверяя и удаляя(если они не нужны) объекты из Old Generation. Как можно заметить, сборщики мусора работают не по одному конкретному алгоритму. Они используют гибридные подходы и умеют в разные моменты времени переключаться на разные алгоритмы.

Я назвал раздел Гипотезы о поколениях, но пока рассказал только об одной из них. Вторая — так называемая сильная гипотеза о поколениях говорит о следующем: объекты из старого поколения редко ссылаются на объекты из молодого поколения.

Заключение.

Следует также помнить, что у каждой JVM может быть свой сборщик мусора, который может реализовывать свой алгоритм. И даже не один. Если вам задали вопрос на собеседовании о том, какой алгоритм сборки мусора используется в Java, спросите, о какой именно JVM и версии идет речь и перечислите наиболее общие понятия и подходы к процедуре сборки мусора.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *