Как устроен и где работает сборщик мусора (GC) в Go?

Ответ

Сборщик мусора (Garbage Collector, GC) в Go работает исключительно с кучей (heap) — областью памяти, где размещаются динамически выделенные объекты (например, через make(), new() или при взятии адреса у составных литералов).

Память на стеке (stack) (локальные переменные функций, аргументы) управляется автоматически и освобождается при выходе из области видимости (при завершении функции). GC ее не касается.

Ключевые характеристики GC в Go:


  • Параллельный (Concurrent): GC работает в фоновом режиме, в отдельных горутинах, параллельно с основной программой. Это позволяет минимизировать паузы (stop-the-world), во время которых выполнение пользовательских горутин полностью останавливается. Паузы в современном Go GC измеряются в микросекундах.


  • Трехцветный алгоритм маркировки и очистки (Tri-color mark-and-sweep): Это алгоритм, который позволяет GC работать параллельно с программой. Объекты условно делятся на три множества:

    • Белые: Объекты-кандидаты на удаление.
    • Серые: Объекты, которые достижимы из корневых объектов (глобальные переменные, стеки горутин), но их дочерние объекты еще не просканированы.
    • Черные: Объекты, которые точно достижимы и все их дочерние объекты просканированы.

      GC ищет и помечает все достижимые объекты (перекрашивая их из белого в серый, а затем в черный), а в конце фазы очистки удаляет все оставшиеся "белые" (недостижимые) объекты.

  • Неперемещающий (Non-moving): В отличие от GC в некоторых других языках (например, Java), Go GC не перемещает объекты в памяти для дефрагментации кучи. Это упрощает написание кода на CGo и взаимодействие с указателями.



  • Управляется через GOGC: Переменная окружения GOGC контролирует, когда запускается следующий цикл сборки мусора. Значение по умолчанию 100 означает, что новый цикл начнется, когда размер кучи после предыдущей сборки увеличится на 100% (т.е. вдвое). GOGC=off отключает GC.


Пример:

func createObject() *int {
    // `make` выделяет память в куче для слайса.
    // Эта память будет управляться GC.
    data := make([]int, 1000)

    // `new` также выделяет память в куче.
    // Указатель на эту память покидает область видимости функции,
    // но сама память останется, пока на нее есть ссылки.
    obj := new(int)
    *obj = 42
    _ = data // Используем data, чтобы компилятор не оптимизировал

    return obj
}

func main() {
    // p ссылается на объект в куче.
    p := createObject()

    // После того как p выйдет из области видимости (или ему будет присвоен nil),
    // объект *int станет недостижимым, и GC сможет его собрать.
    fmt.Println(*p)
}