Как работает Garbage Collector в Java?

Как работает Garbage Collector в Java?

https://t.me/faangmaster

Это достаточно большая тема. По ней может быть множество вопросов на собеседовании. Я рассмотрю самые популярные и базовые.

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

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

Как GC определяет, какие объекты удалять, а какие нет?

Он начинает с так называемый рутовых объектов (GC Roots). К ним относятся: загруженные классы, статические поля, активные потоки, JNI ссылки, локальные переменные (Stack). Далее он проходит по всему графу объектов связанных с GC Roots ссылками и помечает их как живые/используемые. Остальные объекты он считает не используемыми. И их можно удалять.

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

Вообще, в Java GC поколенческий (Generational GC). Что это значит и зачем это нужно?

Как показали исследования, подавляющее число объектов в Java приложении живут очень мало. Они создаются, быстро используются и перестают быть нужными. А те, которые пережили первые несколько сборок мусора - живут очень долго, практически до конца работы приложения.

Поэтому имеет смысл пристально смотреть на объекты, которые были созданы недавно, потому что они с большой вероятностью не будут нужны почти сразу. А объекты, которые пережили несколько сборок мусора, проверять реже. Поэтому память в Java разбита на поколения и есть Minor и Major сборка мусора.

Рассмотрим устройство памяти в Java. Я изобразил области памяти Java программы, которые интересны для данной темы (области памяти для Native Method Stack, Stack, PC Registers я не рассматриваю).

Объекты в Java хранятся в Java Heap. Он разбит на две части/два поколения:

Young Generation и Old Generation (молодое и старое поколение).

Еще есть MetaSpace, который стал использоваться вместо Permanent (постоянное поколение) начиная с Java 8. Там хранятся: загруженные классы, статические методы, ссылки на статические объекты, примитивные переменные, JIT информация и т.д. Отличие MetaSpace и PermGen в том, что MetaSpace динамический, он может динамически менять свой размер, что предотвращает OutOfMemory в некоторых случаях.

Young Generation разбито на три области: Eden, S0, S1. S0, S1 называют Survivor Space (область выживших). Еще S0 называют "from Survivor Space", а S1 "to Survivor Space".

Когда вы создаете новый объект в Java при помощи new .. объект будет создан в Eden. Когда Eden будет заполнен, GC вызовет Minor GC (GC на Young Generation). Он пометит, все используемые объекты и переместит их в S0 область. Увеличит им счетчик того, сколько раз они пережили сборку мусора. Операция compact также компактифицирует объекты в областях после операции разметки (mark). Вместе, это называется mark-compact операцией. Чем-то похоже на дефрагментацию диска, если вы знаете, что это такое.

Когда Eden заполнится в следующий раз, то выжившие объекты будут перемещены из Eden в S1. А также из S0 в S1. В этом смысле, S0 и S1 поменяются местами. Выжившим объектам будет увеличен счетчик выживаний. После некоторого порога объекты из Survivor Space начнут перемещаться в Tenured/Old Generation. При определенной заполненности Old Generation может быть вызван Major GC на Old Generation. Он обычно работает медленнее, т.к. обычно Old Generation больше по размерам.

Stop-the-world фаза GC это когда вся Java программа останавливается, чтобы выполнить операции связанные со сборкой мусора.

Можно тут почитать более детально про работу GC: https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

Какие бывают Garbage Collectors?

Serial collector

Serial collector выполняет всю свою работу в одном потоке. Использование одного потока может повысить эффективность, поскольку между несколькими потоками не возникает дополнительных затрат на связь. Serial collector лучше всего подходит для однопроцессорных машин. Например, на клиентах или embeded устройствах. Его также можно использовать если нет требований на маленькие задержки или размер Java Heap маленький.

Parallel collector

Его также называют throughput GC. Т.к. он оптимизирует throughput, но при этом допускает большие задержки (latency). Выполняется в нескольких потоках параллельно. Подходит для backend приложений, которым не нужно быстрое время отклика. Например, когда надо перемолоть огромную пачку данных за несколько часов, и иметь быстрый отклик на пользовательские запросы не требуется.

Garbage-first (G1) collector

Оптимизирует одновременно throughput и latency. Работает параллельно с выполнением приложения и старается минимизировать stop-the-world фазы. Подходит для server части приложений с большими Java Heap и большим числом процессоров/ядер, для приложений, где время отклика должно быть маленьким (сотни миллисекунд).

Z Garbage Collector (ZGC)

Оптимизирован на очень малое время откликов для гиганских размеров Java Heap (много терабайт).

Concurrent Mark Sweep collector (deprecated)

Concurrent Mark Sweep (CMS) (или еще известен как  concurrent low pause collector) работает на old generation. Он пытается минимизировать паузы из-за сборки мусора, выполняя большую часть работы по сборке мусора одновременно с потоками приложения. Начиная с Java 9 он deprecated. Рекомендуется использовать G1 вместо него.

Некоторые параметры для настройки GC: https://www.baeldung.com/jvm-parameters

Литература:

https://developers.redhat.com/articles/2021/11/02/how-choose-best-java-garbage-collector

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

https://www.baeldung.com/java-permgen-metaspace

https://www.baeldung.com/java-gc-roots

https://www.baeldung.com/java-string-pool







Report Page