Ако сте запознати с основите на разпределението на паметта в езиците за програмиране, знаете, че има две части в паметта, определени като Heap и Stack.

Паметта на стека се използва за изпълнение на нишка. Когато се извика функция, в стека се разпределя блок памет за съхраняване на локалните променливи на функцията. Разпределената памет се освобождава, когато функцията се върне. За разлика от стека, Heap паметта се използва за динамично разпределение (обикновено при създаване на обекти с ключова дума „new“ или „malloc“) и освобождаването на паметта трябва да се обработва отделно.

Object myObject = new Object();

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


myObject = null;

Ако в някакъв момент от програмата друга препратка към обект или „null“ е била присвоена на променливата „myObject“, препратката, която съществува с вече създадения „Object“, ще бъде премахната. Въпреки това паметта, разпределена за този „обект“, няма да бъде освободена, въпреки че обектът не се използва. В по-старите програми, като C или C++, програмистът трябва да се грижи за този тип обекти, разпределени в Heap, и да ги изтрива, когато не се използват, за да освободи паметта. Ако не направите това, това може да доведе до изтичане на памет. От друга страна, ако по погрешка изтрием обект, който има жива препратка към променлива, това може да причини изключения за нулев указател в по-късните части на кода, когато се опитаме да осъществим достъп до изтрития обект, използвайки старата препратка.

Въпреки това, в езици като Java и C#, това управление на паметта се управлява от отделен обект, известен като Garbage Collector.

С поставен Garbage Collector можем да разпределим обект в паметта, да го използваме и когато вече няма референция за този обект, обектът ще бъде маркиран за Garbage Collector, за да го вземе, освобождавайки разпределената памет. А Garbage Collector също гарантира, че всеки жив обект, който има активна препратка, няма да бъде премахнат от паметта.

Референтно преброено събиране на отпадъци

Събирането на боклука за броя на препратките следи броя на препратките за определен обект в паметта. Нека да разгледаме следния кодов сегмент.

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;
    }
}

Когато присвоим нулеви стойности за двете променливи едно и две, външните препратки, съществуващи с обектите от клас („A“ и „B“), създадени в началото, ще бъдат премахнати. Все пак те няма да отговарят на условията за събиране на боклук, тъй като броячите на референтните обекти на тези два обекта няма да станат нула поради обекта „A“, който има препратка вътре в „B“, а обектът „B“ има своята препратка вътре в „A“.

Маркирайте и почистете събирането на боклука

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

Фаза на маркиране
По време на фазата на маркиране GC идентифицира обектите, които все още се използват, и задава техния „бит за маркиране“ на true. Търсенето започва с основен набор от препратки, съхранявани в локални променливи в стека или глобални променливи. Започвайки от основните препратки, GC ще извърши търсене в дълбочина за обектите, които имат достъпни препратки от корена. Всеки обект, който поддържа препратка към друг обект, поддържа този обект жив.

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

Цикличните препратки не са проблем за GC за маркиране и почистване. Ако наблюдавате горната диаграма, съществува циклична препратка (показана с квадрат), но тя е недостижима от корена. Следователно тези типове препратки няма да бъдат маркирани като активни, позволявайки на GC да събира като боклук.

Фаза на почистване
В повърхността на почистване всички немаркирани обекти от фазата на маркиране ще бъдат премахнати от паметта, освобождавайки място.

Както можете да видите от горната диаграма, може да има много свободни региони след фазата на почистване. Но поради тази фрагментация следващото разпределение на паметта може да се провали, ако е по-голямо от всички съществуващи свободни региони.

За да се преодолее този проблем, беше добавена допълнителна Компактна фаза.

Mark-Sweep-Compact Garbage Collection

След фазата на почистване всички места в паметта се пренареждат, за да осигурят по-компактно разпределение на паметта. Недостатъкът на този подход е увеличената продължителност на паузата на GC, тъй като трябва да копира всички обекти на ново място и да актуализира всички препратки към такива обекти.

Маркиране и копиране на Garbage Collector

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

По време на фазата на копиране, маркираните обекти се копират в другото пространство (tospace) и в същото време се уплътняват. След това от пространството се изчиства.

След това и двете пространства се разменят, което води до всяко ново разпределение на паметта за разпределяне на памет в „новото fromspace“ (старото tospace сега ще стане новото fromspace). Накрая, когато „новото от космоса“ се напълни, целият процес се повтаря отново.

Generation Garbage Collector

При събирането на отпадъци за поколенията пространството на паметта е разделено на различни поколения (напр. младо поколение и старо поколение). Първоначално всички обекти ще бъдат на младото поколение. Въпреки това, когато се случи цикъл на събиране на боклука, обектите, които оцелеят след събирането на боклука, ще бъдат популяризирани към по-старото поколение.

Сега обектите, останали в младото поколение, могат да бъдат изчистени, тъй като всички живи обекти се преместват в старото поколение.

Циклите на събиране на боклука в старото поколение се случват по-рядко, отколкото в младото поколение. Основната идея зад този подход е, че обектите, които оцеляват след първото събиране на боклук, са склонни да живеят по-дълго. По този начин честотата на събиране на боклука може да бъде намалена за обекти от по-старите поколения. Броят на поколенията се различава в зависимост от езика за програмиране. Например в 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