itmo_conspects

Лекция 4

Сборка мусора - процесс восстановления заполненной памяти среды выполнения путем уничтожения неиспользуемых объектов

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

void foo() {
    // выделили память для 100 символов
    char* array = new char[100];

    // выделили память еще раз, перезаписали старый указатель, 
    // те самым потеряв к старому массиву доступ 
    array = new char[100];
}

Чтобы облегчить жизнь программиста и направить всю его концентрацию на создание бизнес-логики, придумали автоматическое управление памятью

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

Перед сборщиком мусора (Garbage Collector) стоят 2 задачи:

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

Обнаружение мусора

Есть 2 способа обнаруживать мусор:

Счетчик ссылок считает количество живых ссылок на объект. Если число ссылок достигает 0, то объект удаляется (подобно shared_ptr в С++). Несмотря на простоту, счетчик ссылок плохо сочетается с многопоточностью и без дополнительных алгоритмов не может выявлять циклические ссылки (объекты ссылаются друг на друга => счетчики не нулевые)

Трейсинг основан на графе объектов и его обхода. Для начала вводится понятие корневой точки (GC Root). Корневой точкой мы будем считать локальные переменные, статические переменные, потоки, ссылки из Java Native Interface и т.д.. Затем строится дерево ссылок. Мусором считается тот объект, до которого нельзя попасть из корневых точек.

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

public House doSomething(string[] args) {
    Person person = new Person("Ivan");

    person.setHouse(new House());
    person.getHouse().setRoof(new Roof());
    person.getHouse().setDoor(new Door());

    Person person1 = new Person("Michael");
    Person person2 = new Person("John");
    person1.setFriend(person2);
    person2.setFriend(person1);

    return person;
}

Получаем такой лес объектов:

 person     
  |- house
      |- roof
      |- door

 person1
  |- person2
      |- ...

 person2
  |- person1
      |- ...

После return объект person остается жить, потому что он был возвращен, а person1 и person2 - нет. Несмотря на то, что количество ссылок на них ненулевое количество, из корневых точек в них попасть мы не можем (а таковым они перестают считаться после return)

Очистка мусора

От сборщика мусора нам нужно, что бы он:

Добиться всех трех свойств нереально, поэтому то, что сейчас есть - это компромиссы между ними

Существует несколько алгоритмов очистки мусора. Один из них - копирующая сборка

Для копирующей сборки память условно делится на две части: from-space и to-space. Сначала объекты попадают в from-space. Когда она заполняется, происходит stop-the-world (остановка мира), сборщик мусора проходится по объектам, копирует нужные объекты в to-space, а ненужные высвобождаются. После этого области памяти from-space и to-space меняются местами (свапаются указатели)

Stop-the-world гарантирует, что во время очистки не выделится память для новых объектов, тем самым граф объектов будет заморожен

Другим методом является “отслеживание и очистка” (Mark and Sweep). При помощи трейсинга сборщик мусора помечает живые объекты и во время остановки мира пробегается по всем объектам и удаляет те, которые не были помечены живыми. После очистки объекты могут располагаться по всей памяти, тем самым фрагментируя ее. Дополнительно может производиться дефрагментация памяти (такой алгоритм называют Mark and Sweep Compact): сдвиг живых объектов в самое начало. Заметим, что дефрагментация - очень дорогая операция.

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

Будем говорить, что быстроживущие объекты принадлежат младшему поколению (young generation), а долгоживущие - старшему поколению (old generation). Наблюдения привели к тому, что большинство объектов принадлежат младшему поколению (итераторы, локальные переменные и т.д.), тогда как если объект принадлежит старшему поколению, то не нужным он будет совсем не скоро.

Поэтому имеет смысл сделать три типа очистки:

Сразу оговоримся, что мы рассматриваем алгоритм, реализованный в HotSpot JVM. Реализация может очень сильно отличаться от виртуальной машины и выбранного сборщика мусора.

Тогда мы можем разделить нашу память на 3 части:

До Java 8 память JVM выглядела так:

picture

Помимо выше указанных существовала область PermGen - постоянное поколение. Там хранились метаданные о классах, и располагалась она на стеке. С Java 8 эту область решили назвать MetaSpace и перенести на кучу

Таким образом, Minor сборка мусора начинается с тех пор, как заполняется Эдем. Из Эдем выжившие объекты переходят в Пространство выживших. В Major сборке очищается хранилище

Сборщики мусора, основывающиеся на поколениях, называют Generational Garbage Collector

Реализации сборщиков мусора

Разберем некоторые реализации сборщиков мусора из HotSpot JVM

Serial GC

Простенький сборщик мусора для однопоточных приложений. Во время работы останавливает все приложение, поэтому не рекомендуется в случае, когда необходимы минимальные задержки. Включается флагом -XX:UseSerialGC в JVM

Parallel GC

Сборщик мусора по умолчанию, работает в несколько потоках, во время работы останавливает все приложение. Включается флагом -XX:UseParallelGC

CMS GC

Concurrent Mark and Sweep сборщик работает как и Parallel GC, только сводится время остановки мира к минимуму засчет большего потребления ресурсов ЦП. CMS GC не выполняет дефрагментацию. Включается флагом -XX:UseConcMarkSweepGC

G1 GC

Garbage 1st GC работает как и CMS GC, только вместо разделения памяти на поколения, память разделена на набор областей, каждая из которых может представлять младшее либо старшее поколение. Используется в Minecraft👍. Включается флагом -XX:UseG1GC

Epsilon GC

Совсем ниче не умеет, используется, когда мусора в вашем коде нет. Сдается, когда память закончилась. Включается флагом -XX:UnlockExperimentalVMOptions -XX:UseEpsilonGC

Shenandoah GC

Работает как G1 GC, только с меньшими задержками и большими затратами на ЦП. Включается флагом -XX:UnlockExperimentalVMOptions -XX:UseShenandoahGC

ZGC

Используется, когда нужны очень маленькие задержки и когда есть очень много оперативной памяти. Использовать лучше на сервере с огромном оперативкой, а не на тостере. Включается флагом -XX:UnlockExperimentalVMOptions -XX:UseZGC


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