Если вы знакомы с основами распределения памяти в языках программирования, вы знаете, что в памяти есть две части, определяемые как куча и стек.

Память стека используется для выполнения потока. Когда функция вызывается, в стеке выделяется блок памяти для хранения локальных переменных функции. Выделенная память освобождается, когда функция возвращается. В отличие от стека, память кучи используется для динамического распределения (обычно при создании объектов с ключевым словом «new» или «malloc»), а освобождение памяти необходимо обрабатывать отдельно.

Object myObject = new Object();

.............


myObject = null;

Если в какой-то момент программы другая ссылка на объект или «null» была присвоена переменной «myObject», ссылка, которая существовала с уже созданным «объектом», будет удалена. Однако память, выделенная для этого «объекта», не будет освобождена, даже если объект не используется. В более старых программах, таких как C или C ++, программист должен заботиться об этих типах объектов, размещенных в куче, и удалять, когда они не используются, чтобы освободить память. Невыполнение этого может привести к утечке памяти. С другой стороны, если мы по ошибке удалим объект, имеющий активную ссылку на переменную, это может вызвать исключения нулевого указателя в более поздних частях кода, когда мы пытаемся получить доступ к удаленному объекту, используя старую ссылку.

Однако в таких языках, как Java и C #, это управление памятью обрабатывается отдельной сущностью, известной как сборщик мусора.

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

Сборка мусора с подсчетом ссылок

Сборка мусора счетчика ссылок отслеживает количество ссылок на конкретный объект в памяти. Давайте посмотрим на следующий фрагмент кода.

Object a = new Object(); // Reference Count(OB1) starts at 1
Object b = a;         // Reference Count (OB1) incremented to 2 as a new reference is added
Object c = new Object();

b.someMethod();
b = null;        // Reference Count(OB1) decremented to 1 as reference goes away
a = c;           // Reference Count(OB1) decremented to 0

При выполнении строки Object a = new Object () в памяти создается новый объект (скажем, OB1), а счетчик ссылок (для OB1) начинается с 1.

Когда ссылка для OB1 в переменной «a» копируется в «b», счетчик ссылок увеличивается на единицу, так как теперь две переменные имеют ссылку для OB1.

Когда «b» присваивается значение null, ссылка на OB1 уменьшается, оставляя только переменную «a», имеющую ссылку на OB1.

Когда значение «a» обновляется значением «c» (которое имеет ссылку для всего нового объекта), счетчик ссылок для OB1 становится нулевым, оставляя OB1 доступным для сборки мусора.

Недостатки GC с подсчетом ссылок

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

Рассмотрим два класса A и B, которые ссылаются друг на друга.

class A {
    private B b;

    public void setB(B b) {
        this.b = b;
    }
}

class B {
    private A a;

    public void setA(A a) {
        this.a = a;
    }
}

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

public class Main {
    public static void main(String[] args) {
        A one = new A();
        B two = new B();

        // Make the objects refer to each other (creates a circular reference)
        one.setB(two);
        two.setA(one);

        // Throw away the references from the main method; the two objects are
        // still referring to each other
        one = null;
        two = null;
    }
}

Когда мы присваиваем нулевые значения двум переменным 1 и 2, внешние ссылки, существовавшие с объектами класса («A» и «B»), созданными в начале, будут удалены. Тем не менее, они не будут иметь право на сборку мусора, поскольку счетчики ссылок этих двух объектов не станут равными нулю из-за того, что объект «A» имеет ссылку внутри «B», а объект «B» имеет ссылку внутри «A».

Отметить и очистить сборку мусора

Как следует из названия, сборщики мусора Mark и Sweep делятся на две фазы
1. Фаза метки
2. Фаза очистки

Фаза метки
Во время фазы метки сборщик мусора идентифицирует объекты, которые все еще используются, и устанавливает для их «бит метки» значение true. Поиск начинается с корневого набора ссылок, хранящихся в локальных переменных в стеке или глобальных переменных. Начиная с корневых ссылок, сборщик мусора будет выполнять поиск в глубину объектов, на которые есть доступные ссылки из корня. Любой объект, который хранит ссылку на другой объект, сохраняет этот объект в живых.

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

Циклические ссылки не являются проблемой для GC Mark and Sweep. Если вы посмотрите на диаграмму выше, циклическая ссылка существует (показана квадратом), но недостижима из корня. Следовательно, эти типы ссылок не будут помечены как активные, что позволит сборщику мусора собирать мусор.

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

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

Чтобы решить эту проблему, была добавлена ​​дополнительная фаза Compact.

Mark-Sweep-Compact Сборщик мусора

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

Отметить и скопировать сборщик мусора

Это похоже на сборщик Mark and Sweep, но пространство памяти разделено на две части. Первоначально объекты размещаются в одном пространстве (fromspace), а живые объекты отмечаются.

На этапе копирования отмеченные объекты копируются в другое пространство (tospace) и одновременно уплотняются. Затем очищается fromspace.

После этого оба пространства меняются местами, что приводит к любому новому выделению памяти для выделения памяти в «новом fromspace» (старый tospace теперь станет новым fromspace). Наконец, когда «новое из пространства» заполняется, весь процесс повторяется снова.

Сборщик мусора поколений

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

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

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

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

использованная литература

[1] https://app.pluralsight.com/library/courses/understanding-java-vm-memory-management/table-of-contents

[2] https://www.geeksforgeeks.org/mark-and-sweep-garbage-collection-algorithm/

[3] https://blogs.msdn.microsoft.com/abhinaba/2009/01/30/back-to-basics-mark-and-sweep-garbage-collection/

[4] https://plumbr.io/handbook/garbage-collection-algorithms