Как GC останавливает весь мир, чтобы вынести мусор

в 8:29, , рубрики: C#, Garbage collection, garbage collector, gc, Go, java, kotlin, scala, swift

Коротко о самом главном в сборке мусора.

⚠️ Важно: это только концептуальная иллюстрация. В реальных средах исполнения JVM, .NET, Go и т.д. все сложнее!

💡 Терминология:

🔹 GC

Garbage Collector, он же сборщик мусора, чистит память от неиспользуемых объектов.

🔹 Heap (Куча)

Heap - это просто область памяти, сюда попадают ссылочные объекты, каждый new Object(), new Array() - выделяет память именно в этой области, память в куче активно дефрагментируется и переиспользуется, чтобы лишний раз не просить ее у операционной системы. Т.к. это уже аллоцированная или зарезервированная память, среда выполнения вашего кода может создавать в ней объекты быстрее. Если памяти в куче нехватает, дополнительная память запрашивается у операционной системы.

🔹Поколения

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

  • Поколение 0 (Young Generation) — для новых объектов. Большинство из них быстро становятся мусором.

  • Поколение 1 (Old Generation) — для объектов, которые "выжили" несколько сборок мусора.

  • Поколение 2 (Tenured Generation) — для старых объектов, которые долго живут.

GC может чистить поколения по очереди, не обязательно за один вызов.

Почему это важно?

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

🔹 LOH - Large Object Heap

Куча больших объектов. Современные GC делят объекты не только по поколениям, но и по размеру, чтобы упростить процесс дефрагментации памяти, большие объекты GC кладет в LOH! И чистит LOH отдельно от маленьких объектов.

🔹 Корни

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

Примеры корней:

  1. Локальные переменные в стеке

  2. Все переменные текущих функций, которые содержат ссылки на объекты в куче.

  3. Регистры процессора

  4. Во время выполнения некоторые ссылки могут храниться прямо в регистрах CPU.

  5. Статические переменные static или global объекты, доступные из любого места программы.

  6. Ссылки из runtime / глобальных структур - например, внутренние структуры runtime, которые хранят активные объекты.

Как GC работает с корнями

  1. GC начинает с корней.

  2. Обходит все ссылки из корней, помечая все достижимые объекты как живые.

  3. Всё, что не достижимо от корней, считается мусором и очищается.

🔹 Stop The World

В управляемых языках это "процесс" остановки ваших потоков, для того, чтобы GC мог почистить память от ненужных объектов.

🔹 GC_POLL()

Фунция (на самом деле кусочек ассемблера), которая встраивается в ваш код и останавливает его исполнение по требованию GC, имя +/- похожее во всех средах с GC.

🔹 Safe points

Места в вашем коде, куда может встроиться функция GC_POLL()

Где Safe points и где может встроиться GC_POLL():

  1. На входе в функцию

  2. В конце горячих циклов (много итераций)

  3. На выходе из функции

  4. После вашего new (аллокации памяти) - если тип ссылочный

  5. После вызовов функций

  6. На определённых точках в коде с долгими вычислениями

  7. По желанию производителей компилятора...

⚠️ Safe points расставляются компилятором, программист напрямую на это не влияет.

⚙️ Примерный порядок действий(реализация зависит от конкретной среды C#, Java, Kotlin, Scala, Swift, Go и прочие):

  1. Компилятор вставляет в Вашу программу функцию и флаг, они доступны из любой точки программы

//Флаг остановки мира, помечена volatile - чтобы чтение было не из кэша процессора, а напрямую из RAM - это важно, потому-что в кэше может быть другое состояние.
volatile bool gc_stop_the_world = false;

//Главная функция, которую будем расставлять по коду
//Модификатор для функции inline - обычно означает, что на место, где функция будет вызвана, просто подставится ее тело, вместо вызова, обычно это просто реккомендация, которую компиляторы могут не соблюдать, но в нашем примере будем считать что компилятор всегд
inline void GC_POLL() {
    if (gc_stop_the_world) {
        //Барьер памяти, который гарантирует, 
        //Что все операции записи завершены и видны потоку GC
		MemoryBarrier();
        //Функция прерывающая выполнение текущего потока кода, 
        //ваш код замирает
        SuspendCurrentThread(); 
    }
}
  1. Расставляет функцию GC_POLL() по вашему коду в безопасных точках (Safe Points)

Ваш исходный код:

void Main()
{
	//...Какой-то ваш код

	for(var i = 0; i < 1000000; ++i)
	{
		//...Какой-то ваш код
	}

	//...Какой-то ваш код
}

Компилируется во что-то типа такого:

//Ваша функция
void Foo(int count)
{
	GC_POLL(); //Вы этого не писали

	//...Какой-то ваш код

	for(var i = 0; i < count; ++i)
	{
		//...Какой-то ваш код

		GC_POLL() //Вы этого не писали
	}

	//...Какой-то ваш код

	GC_POLL(); //Вы этого не писали
}

⚠️ Важно: компилятор вставляет минимальный ассемблерный код, а не прям call функции GC_POLL(), вызов GC_POLL() - представлен для урощения.

  1. При старте вашей программы запускается поток Вашей программы и отдельный поток/и GC, например такая функция:

//Функция самого GC (Сборщик мусора, он же Garbage Collector)
//Она работает параллельно с вашей программой
void GC_WORK()
{
    while(true)
    {
        //Проверяем надо ли чистить память
        //Реализация - да какая угодно
        if(NeedToClearMemory()) 
        { 
            //Выставляем флаг "остановки мира"
            gc_stop_the_world = true;

            //Просто ждем когда все нужные потоки остановятся
            //Тоже какая угодно реализация
            WaitStopTheWorld();
            
            //Тут происходит очистка памяти
            //Чистим все объекты на которые потеряли ссылки
            ClearMemory();

            //Сбрасывает флаг "остановки мира"
            gc_stop_the_world = false;

            //Запускает Ваши потоки в работу
            RunTheWorld();
        }
        else
        {
            Sleep(1); //Например можно засыпать на 1 мс
        }
    }
}
  1. Когда GC решает что памяти мало, он поднимает флаг gc_stop_the_world, а потоки самостоятельно доходят до Safe Point и приостанавливаются, runtime координирует этот процесс.

  2. GC ждет остановку всех ваших потоков.

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

  4. Сбрасывает флаг gc_stop_the_world.

  5. Возобновляет работу Ваших потоков.

⚠️ Ваш код сам может вызвать работу GC, если вы вызвали аллокацию (создали новый управляемый объект), а свободной памяти у менеджера памяти нет:

    var x = new byte[1000]; // Этот код может сам заставить GC выполнить работу, потому-что на самом деле вызывается код аллокатора памяти и просит минимум 1000 байт

⚠️ Некоторые потоки могут быть неуправляемыми или сообщеить GC, что в данный момент их останавливать не нужно, тогда GC пропускает их при проверке.

Автор: MiniBytes

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js