Коротко о самом главном в сборке мусора.
⚠️ Важно: это только концептуальная иллюстрация. В реальных средах исполнения 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 отдельно от маленьких объектов.
🔹 Корни
Корни - это набор переменных и объектов, которые доступны напрямую из исполняемого кода и считаются живыми. Все объекты, достижимые через цепочку ссылок от корней, тоже считаются живыми. Всё остальное может быть очищено сборщиком мусора.
Примеры корней:
-
Локальные переменные в стеке
-
Все переменные текущих функций, которые содержат ссылки на объекты в куче.
-
Регистры процессора
-
Во время выполнения некоторые ссылки могут храниться прямо в регистрах CPU.
-
Статические переменные static или global объекты, доступные из любого места программы.
-
Ссылки из runtime / глобальных структур - например, внутренние структуры runtime, которые хранят активные объекты.
Как GC работает с корнями
-
GC начинает с корней.
-
Обходит все ссылки из корней, помечая все достижимые объекты как живые.
-
Всё, что не достижимо от корней, считается мусором и очищается.
🔹 Stop The World
В управляемых языках это "процесс" остановки ваших потоков, для того, чтобы GC мог почистить память от ненужных объектов.
🔹 GC_POLL()
Фунция (на самом деле кусочек ассемблера), которая встраивается в ваш код и останавливает его исполнение по требованию GC, имя +/- похожее во всех средах с GC.
🔹 Safe points
Места в вашем коде, куда может встроиться функция GC_POLL()
Где Safe points и где может встроиться GC_POLL():
-
На входе в функцию
-
В конце горячих циклов (много итераций)
-
На выходе из функции
-
После вашего new (аллокации памяти) - если тип ссылочный
-
После вызовов функций
-
На определённых точках в коде с долгими вычислениями
-
По желанию производителей компилятора...
⚠️ Safe points расставляются компилятором, программист напрямую на это не влияет.
⚙️ Примерный порядок действий(реализация зависит от конкретной среды C#, Java, Kotlin, Scala, Swift, Go и прочие):
-
Компилятор вставляет в Вашу программу функцию и флаг, они доступны из любой точки программы
//Флаг остановки мира, помечена volatile - чтобы чтение было не из кэша процессора, а напрямую из RAM - это важно, потому-что в кэше может быть другое состояние.
volatile bool gc_stop_the_world = false;
//Главная функция, которую будем расставлять по коду
//Модификатор для функции inline - обычно означает, что на место, где функция будет вызвана, просто подставится ее тело, вместо вызова, обычно это просто реккомендация, которую компиляторы могут не соблюдать, но в нашем примере будем считать что компилятор всегд
inline void GC_POLL() {
if (gc_stop_the_world) {
//Барьер памяти, который гарантирует,
//Что все операции записи завершены и видны потоку GC
MemoryBarrier();
//Функция прерывающая выполнение текущего потока кода,
//ваш код замирает
SuspendCurrentThread();
}
}
-
Расставляет функцию 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() - представлен для урощения.
-
При старте вашей программы запускается поток Вашей программы и отдельный поток/и 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 мс
}
}
}
-
Когда GC решает что памяти мало, он поднимает флаг gc_stop_the_world, а потоки самостоятельно доходят до Safe Point и приостанавливаются, runtime координирует этот процесс.
-
GC ждет остановку всех ваших потоков.
-
Чистит память используя корни, поколения, потерю достежимости до корней, разные кучи для объектов разного размера
-
Сбрасывает флаг gc_stop_the_world.
-
Возобновляет работу Ваших потоков.
⚠️ Ваш код сам может вызвать работу GC, если вы вызвали аллокацию (создали новый управляемый объект), а свободной памяти у менеджера памяти нет:
var x = new byte[1000]; // Этот код может сам заставить GC выполнить работу, потому-что на самом деле вызывается код аллокатора памяти и просит минимум 1000 байт
⚠️ Некоторые потоки могут быть неуправляемыми или сообщеить GC, что в данный момент их останавливать не нужно, тогда GC пропускает их при проверке.
Автор: MiniBytes
