Глубокое понимание сборки мусора на языке Go

Go
Глубокое понимание сборки мусора на языке Go

7.2 Сборщик мусора

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

Практически во всех современных языках программирования сборщик мусора представляет собой сложную систему. Требуются большие усилия, чтобы вернуть отброшенную память, не затрагивая пользовательскую программу. Хороший пример — механизм сборки мусора в Java. mark-sweep и G1 четыре сборщика мусора1, требуется много усилий, чтобы понять, как они работают и детали реализации.

В этом разделе будут подробно представлены принципы проектирования и реализации сборщика мусора в системе выполнения языка Go.Мы не только обсудим общий механизм сборки мусора, проанализируем его эволюцию от версии v1.0 языка Go, но и в -глубокий анализ исходного кода Как работает сборщик мусора; Далее мы переходим к другой важной части управления памятью в Go — сборке мусора.

7.2.1 Принцип конструкции

Современные языки программирования обычно управляют памятью как вручную, так и автоматически, такие языки программирования, как C, C++ и Rust, управляют памятью вручную.2, инженеры должны активно запрашивать или освобождать память; в то время как такие языки, как Python, Ruby, Java и Go, используют автоматические системы управления памятью, как правило, механизмы сборки мусора, а Objective-C выбирает автоматический подсчет ссылок3, хотя подсчет ссылок также является механизмом автоматического управления памятью, мы не будем подробно рассматривать его здесь, а основное внимание в этом разделе уделяется сборке мусора.

Я считаю, что у многих впечатление от сборщика мусора состоит в том, чтобы остановить программу (Остановить мир, STW).По мере того, как пользовательская программа требует все больше и больше памяти, мусор в системе постепенно увеличивается; когда использование памяти программой достигает определенный порог, все приложение будет приостановлено, сборщик мусора просканирует все выделенные объекты и вернет пространство памяти, которое больше не используется. Когда этот процесс закончится, пользовательская программа может продолжить выполнение. Язык Go также использовал эту стратегию в первые дни Реализовал сборку мусора, но сегодняшние реализации намного сложнее.

mutator-allocator-collector

Рисунок 7-21 Компоненты управления памятью

На приведенном выше рисунке пользовательская программа (Mutator) будет запрашивать память в куче через распределитель памяти (Allocator), а сборщик мусора (Collector) отвечает за освобождение памяти в куче. коллектор совместно управлять программой.куча пространства памяти в . В этом разделе мы подробно рассмотрим ключевые теории, связанные со сборкой мусора в Go, чтобы помочь нам лучше понять оставшуюся часть этого раздела.

пометить как очищенный

Алгоритм Mark-Sweep является наиболее распространенным алгоритмом сборки мусора.Сборщик Mark-Sweep является отслеживающим сборщиком мусора.Процесс его выполнения можно разделить на два этапа: Mark и Sweep:

  1. Фаза маркировки — начиная с корневого объекта, чтобы найти и отметить все оставшиеся объекты в куче;
  2. Фаза очистки — пройтись по всем объектам в куче, освободить непомеченные мусорные объекты и добавить освобожденную память в список свободных;

Как показано на рисунке ниже, пространство памяти содержит несколько объектов.Мы начинаем с корневого объекта и по очереди обходим подобъекты объекта и помечаем все объекты, достижимые из корневого узла, как живые, а именно три объекта A, C и D, а остальные три объекта B, E и F считаются мусором, поскольку они недоступны из корневого узла:

mark-sweep-mark-phase

Рисунок 7-22 Фаза маркировки функции «Отметить очистку»

После того, как этап маркировки завершен, он переходит к фазе очистки.На этом этапе сборщик по очереди обходит все объекты в куче, освобождает три объекта B, E и F, которые не были отмечены, и подключает новые свободное место в памяти в структуре связанного списка.Вверх, удобно использовать распределитель памяти.

mark-sweep-sweep-phase

Рисунок 7-23. Фаза очистки для отметки «Отмена».

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

трехцветная абстракция

Чтобы устранить длительное время STW, вызванное исходным алгоритмом метки-очистки, большинство современных сборщиков мусора с отслеживанием реализуют вариант алгоритма трехцветной метки для сокращения времени STW. Алгоритм трехцветной маркировки делит объекты в программе на три категории: белые, черные и серые.4:

  • белые объекты - потенциальный мусор, память которого может быть освобождена сборщиком мусора;
  • Черные объекты — живые объекты, в том числе объекты, не имеющие ссылок на внешние указатели и объекты, достижимые из корневого объекта;
  • Серые объекты — живые объекты, так как есть внешние указатели на белые объекты, сборщик мусора просматривает дочерние объекты этих объектов;

tri-color-objects

Рисунок 7-24 Трехцветный объект

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

tri-color-mark-sweep

Рисунок 7-25 Процесс выполнения сборщика мусора с трехцветной маркировкой

Принцип работы сборщика мусора с трехцветной маркировкой очень прост, и мы можем свести его к следующим шагам:

  1. Выберите серый объект из набора серых объектов и пометьте его черным;
  2. Отметьте все объекты, на которые указывает черный объект, как серые, чтобы гарантировать, что ни объект, ни объект, на который ссылается объект, не будут переработаны;
  3. Повторяйте два вышеуказанных шага до тех пор, пока в графе объектов не останется серых объектов;

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

tri-color-mark-sweep-after-mark-phase

Рисунок 7-26 Куча после трехцветной маркировки

Поскольку пользовательская программа может изменить указатель объекта в процессе выполнения маркировки, сам алгоритм очистки трехцветной метки не может выполняться одновременно или инкрементно.Он по-прежнему требует STW.Во время процесса трехцветной маркировки, как показано ниже, пользовательская программа. С объекта A устанавливается ссылка на объект D, но поскольку серый объект больше не существует в программе, объект D будет неправильно собран сборщиком мусора.

tri-color-mark-sweep-and-mutator

Рисунок 7-27 Трехцветная метка и пользовательская программа

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

барьерная технология

Технология барьера памяти — это барьерная инструкция, которая позволяет ЦП или компилятору следовать определенным ограничениям при выполнении операций, связанных с памятью.Большинство современных процессоров в настоящее время выполняют инструкции не по порядку, чтобы максимизировать производительность, но эта технология может гарантировать код для последовательного характера памяти операции, операции, выполняемые до барьера памяти, должны предшествовать операциям, выполняемым после барьера памяти.6.

Чтобы обеспечить правильность параллельных или инкрементных алгоритмов маркировки, нам необходимо достичь любого из следующих двух трехцветных инвариантов:

  • Сильная трихроматическая инвариантность - черные объекты не указывают на белые объекты, а только на серые или черные объекты;
  • Слабая трихроматическая инвариантность - белый объект, на который указывает черный объект, должен содержать достижимый путь от серого объекта через несколько белых объектов.7;

strong-weak-tricolor-invariant

Рисунок 7-28 Трехцветная инвариантность

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

Техника барьера в сборке мусора больше похожа на метод ловушки.Это фрагмент кода, который выполняется, когда пользовательская программа читает объект, создает новый объект и обновляет указатель объекта.В зависимости от типа операции мы можем разделить их на барьеры чтения (Read Barrier) и барьер записи (Write Barrier), поскольку барьеру чтения необходимо добавлять фрагменты кода в операции чтения, что оказывает большое влияние на производительность пользовательской программы, поэтому языки программирования часто используйте барьеры записи для обеспечения трехцветной инвариантности.

Здесь мы хотим представить две технологии барьера записи, используемые в языке Go, а именно барьер записи вставки, предложенный Дейкстрой.8и предложение Юасы убрать барьер записи9, здесь мы разберем, как они гарантируют трехцветную инвариантность и корректность сборщика мусора.

Вставить барьер записи

Дейкстра предложил вставить барьеры записи в 1978 году. Через барьеры записи, как показано ниже, пользовательская программа и сборщик мусора могут работать попеременно, чтобы обеспечить правильность выполнения программы:

writePointer(slot, ptr):
    shade(ptr)
    *field = ptr

Вышеуказанный псевдокод для вставки барьера записи довольно легко понять, когда мы делаем что-то вроде*slot = ptrвыражение, мы выполним указанный выше барьер записи черезshadeФункция пытается изменить цвет указателя. еслиptrЕсли указатель белый, функция делает объект серым, в противном случае он остается прежним.

dijkstra-insert-write-barrier

Рисунок 7-29 Дейкстра вставляет барьер записи

Предполагая, что мы используем барьер записи вставки, предложенный Дейкстрой в нашем приложении, в сценарии, где сборщик мусора и пользовательская программа запускаются попеременно, будет происходить процесс маркировки, показанный на рисунке выше:

  1. Сборщик мусора помечает корневой объект, указывающий на объект A, черным, а объект B, на который указывает объект A, — серым;
  2. Программа пользователя изменяет указатель объекта A и направляет указатель, первоначально указывающий на объект B, на объект C. В это время срабатывает барьер записи, чтобы пометить объект C как серый;
  3. Сборщик мусора по очереди проходит другие серые объекты в программе, помечая их как черные;

Вставка барьера записи Дейкстры представляет собой относительно консервативную барьерную технологию, котораяОбъекты, которые могут выжить, отмечены серым цветом.Для того, чтобы соответствовать строгой трихроматической инвариантности. Во время процесса сборки мусора, показанного выше, объекты B, которые на самом деле больше не существуют, не собираются окончательно; и если мы изменим указатель на объект C обратно, чтобы он указывал на B между вторым и третьим этапами, сборщик мусора все еще будет считать, что Объекты C живы, эти неправильно помеченные мусорные объекты будут собраны только в следующем цикле.

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

снять барьер записи

Юаса предложил удалить барьер записи в своей статье 1990 года «Сборка мусора в реальном времени на машинах общего назначения», потому что как только барьер записи начинает работать, он гарантирует доступность всех объектов в куче, когда барьер записи включен, поэтому Также используется сборка мусора моментальных снимков (Snapshot GC).10:

This guarantees that no objects will become unreachable to the garbage collector traversal all objects which are live at the beginning of garbage collection will be reached even if the pointers to them are overwritten.

Алгоритм гарантирует корректность программы для инкрементной или параллельной сборки мусора с использованием барьеров записи следующим образом:

writePointer(slot, ptr)
    shade(*slot)
    *slot = ptr

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

yuasa-delete-write-barrier

Рисунок 7-29 Yuasa удаляет барьер записи

Предполагая, что мы используем барьер удаления записи, предложенный Юасой в приложении, в сценарии, когда сборщик мусора и пользовательская программа запускаются попеременно, появится процесс маркировки, показанный на рисунке выше:

  1. Сборщик мусора помечает корневой объект, указывающий на объект A, черным, а объект B, на который указывает объект A, — серым;
  2. Пользовательская программа для исходного указателя объекта на точку B, C, вызывающая удаление барьера записи, но поскольку объект B уже серый, ничего не нужно менять;
  3. Пользовательская программа удаляет указатель, который объект B изначально указывал на C, вызывая удаление барьера записи, и белый объект C окрашивается в серый цвет.;
  4. Сборщик мусора по очереди проходит другие серые объекты в программе, помечая их как черные;

Третий шаг в описанном выше процессе запускает затенение Yuasa для удаления барьера записи, поскольку пользовательская программа удаляет указатель B на объект C, поэтому два объекта C и D нарушат строгую трехцветную инвариантность и слабую трехцветную инвариантность соответственно. :

  • Строгая трихроматическая инвариантность - черный объект A указывает прямо на белый объект C;
  • Слабая трехцветная инвариантность — сборщик мусора не может стартовать с серого объекта и обращаться к белым объектам C и D через несколько последовательных белых объектов;

Yuasa устраняет барьер записи, окрашивая объект C, чтобы гарантировать, что объект C и нижестоящий объект D могут пережить этот цикл сборки мусора, избегая оборванных указателей, чтобы гарантировать правильность пользовательской программы.

Инкрементный и параллельный

Традиционный алгоритм сборки мусора приостанавливает приложение во время выполнения сборки мусора. После запуска сборки мусора сборщик мусора вытесняет использование ЦП и занимает большое количество вычислительных ресурсов для завершения работы по маркировке и очистке.Однако , многие приложения, использующие длинные STW в реальном времени, неприемлемы.

stop-the-world-collector

Рисунок 7-30 Сборка мусора и программа паузы

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

  • Инкрементная сборка мусора — постепенно помечает и удаляет мусор, сокращая максимальное время приостановки приложения;
  • Параллельная сборка мусора. Используйте преимущества многоядерных вычислительных ресурсов для одновременной пометки и удаления мусора во время выполнения пользовательских программ.

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

Инкрементный коллектор

Инкрементная сборка мусора — это решение, позволяющее сократить самое длинное время паузы программы.Он может разделить первоначально долгое время паузы на несколько меньших временных срезов GC, хотя время от начала до конца сборки мусора больше, но это также уменьшает максимальное время приостановки приложения:

incremental-collector

Рисунок 7-31 Инкрементный сборщик мусора

Следует отметить, что инкрементальную сборку мусора необходимо использовать вместе с методом трехцветной маркировки.Для обеспечения корректности сборки мусора нам необходимо открыть барьер записи до начала сборки мусора, чтобы модификация памяти пользовательская программа будет написана первой.Обработка барьера обеспечивает сильную трехцветную инвариантность или слабую трехцветную инвариантность отношения объектов в куче памяти. Хотя добавочная сборка мусора может сократить максимальное время паузы программы, добавочная сборка также увеличивает общее время цикла GC. Во время сборки мусора пользовательские программы также должны нести дополнительные вычислительные затраты из-за влияния барьеров записи. Таким образом, добавочная сборка мусора не единственное преимущество.

параллельный коллектор

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

concurrent-collector

Рисунок 7-31 Параллельный сборщик мусора

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

7.2.2 Процесс эволюции

Сборщик мусора языка Go развивается с первого дня своего рождения, за исключением нескольких версий без крупных обновлений, почти каждая выпущенная минорная версия будет улучшать производительность сборки мусора, а производительность будет улучшаться вместе с ней. Учитывая сложность кода сборщика мусора, в этом разделе будет проанализирована эволюция сборщика мусора по сравнению с версией языка Go v1.0.

  1. v1.0— полностью последовательная пометка и четкий процесс, требующий приостановки всей программы;
  2. v1.1— Параллельно выполнять этапы маркировки и очистки сборки мусора на многоядерных хостах.11;
  3. v1.3- время выполнения на основеТолько значения типа pointer содержат указателиПредположение добавляет поддержку точного сканирования памяти стека, обеспечивая действительно точную сборку мусора.12;
    • Будуunsafe.PointerЗначение, преобразованное в целочисленный тип, считается недопустимым, что может вызвать серьезные проблемы, такие как висячие указатели;
  4. v1.5- Реализовано на основеПараллельность разверток трехцветных маркеровуборщик мусора13;
    • Значительно сократить задержку сборки мусора с нескольких сотен мс до менее 10 мс;
    • Рассчитать подходящее время для запуска сборки мусора и ускорить процесс сборки мусора за счет параллелизма;
  5. v1.6- Достигнутодецентрализациякоординатор по вывозу мусора;
    • На основе явного конечного автомата любая горутина может инициировать переход состояния сборки мусора;
    • Интенсивное использование альтернативного растрового представления списка свободной кучи, что снижает стадию очистки загрузки ЦП.14;
  6. v1.7- пройти черезПараллельное сжатие стекаСокращение времени сборки мусора до менее 2 мс15;
  7. v1.8- использоватьсмешанный барьер записиСокращение времени сборки мусора до менее 0,5 мс16;
  8. v1.9— полностью убрать процесс пересканирования стека остановленной программы17;
  9. v1.10— Обновлена ​​реализация Garbage Collection Pacer для разделения целей жесткого и мягкого размера кучи.18;
  10. v1.12- использоватьАлгоритм завершения новой меткиУпростить несколько этапов сборщика мусора19;
  11. v1.13— Решить проблему возврата памяти в операционную систему для приложений с временным высоким использованием памяти с помощью нового Scavenger20;
  12. v1.14— использовать новый распределитель страницОптимизировать скорость выделения памяти21;

Из эволюции сборщика мусора языка Go видно, что реализация и алгоритм этого компонента становятся все более и более сложными.Первоначальный сборщик мусора все еще был неточным однопоточным сборщиком STW, но последняя версия сборщика мусора Поддерживаются такие функции, как одновременная сборка мусора, децентрализованная координация и т. д., и здесь мы представим компоненты и функции, связанные с последней версией сборщика мусора.

одновременная сборка мусора

Язык Go представил параллельный сборщик мусора в версии 1.5.Сборщик мусора использует трехцветную абстракцию и технологию барьера записи, о которой мы упоминали выше, чтобы гарантировать правильность выполнения сборщика мусора.Как реализовать параллельный сборщик мусора в подробности здесь, давайте взглянем на рабочий процесс некоторых параллельных сборщиков мусора.

Во-первых, параллельный сборщик мусора должен запускать цикл сборки мусора в нужный момент.Если предположить, что наша программа на языке Go работает на 4-ядерной физической машине, то после запуска сборки мусора сборщик будет занимать 25% вычислительных ресурсов. в фоновом режиме для сканирования и маркировки объектов в памяти:

golang-concurrent-collector

Рисунок 7-32 Параллельная коллекция на языке Go

Параллельный сборщик мусора языка Go приостанавливает программу, чтобы сделать некоторые приготовления для маркировки объектов перед сканированием объектов, включая запуск фоновой маркировки сборщика мусора и открытие барьера записи.Если сборщик мусора, выполняемый в фоновом режиме, недостаточно быстр, Приложение подает заявку на Когда скорость памяти превышает ожидаемую, среда выполнения позволит приложению, запрашивающему память, помочь в завершении фазы сканирования сборки мусора. будут постепенно перерабатываться.

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

v1.6 представляет механизм координации децентрализованной сборки мусора.22, превращает сборщик мусора в явный конечный автомат, и любая горутина может вызывать методы для запуска миграции состояния.Обычные методы миграции состояния включают следующие

  • runtime.gcStart- от_GCoffПеревести в_GCmarkэтап, войдите в этап параллельной маркировки и откройте барьер записи;
  • runtime.gcMarkDone— если все доступные объекты просканированы, вызватьruntime.gcMarkTermination;
  • runtime.gcMarkTermination- от_GCmarkконвертировать_GCmarkterminationэтап, войдите в этап завершения отметки и войдите, когда закончите_GCoff;

Вышеупомянутые три методаruntime: replace GC coordinator with state machineПредставленные в связанных с проблемами коммитах, они удаляют децентрализованный процесс перехода состояния.

переработать цель кучи

Хотя сборщику мусора STW необходимо приостановить работу программы, он может эффективно контролировать размер кучи памяти.Конфигурация по умолчанию среды выполнения языка Go запускает новый цикл сборки мусора, когда объем памяти кучи достигает 2-кратного размера предыдущей сборки мусора. поведение может быть достигнуто с помощью переменных окруженияGOGCAdjustment, по умолчанию его значение равно 100, то есть 100% увеличение памяти кучи вызовет GC.

stop-the-world-garbage-collector-heap

Рисунок 7-33 Время сбора мусора сборщиком мусора STW

Поскольку параллельный сборщик мусора будет работать вместе с программой, он не может точно контролировать размер кучи памяти. Параллельный сборщик мусора должен инициировать сборку мусора до достижения цели, чтобы обеспечить контролируемый размер памяти. Гарантированная память кучи в конце сборка мусора такая же, как настроенная пользователемGOGCПоследовательный.

concurrent-garbage-collector-heap

Рисунок 7-34 Память кучи для параллельных сборщиков

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

Алгоритм синхронизации сборки мусора был представлен в версии 1.5. Целью этого алгоритма является оптимизация скорости роста кучи и использования ЦП сборщиком мусора.23, а в версии v1.10 алгоритм был оптимизирован, и исходный размер целевой кучи был разделен на две цели: мягкую и жесткую.24, потому что для настройки частоты выполнения сборки мусора используются более сложные формулы, которые мало помогают в понимании принципов сборки мусора.Этот раздел не будет вводить его, и заинтересованные читатели могут прочитать его самостоятельно.

смешанный барьер записи

До версии Go 1.7 среда выполнения использовала Dijkstra для вставки барьеров записи, чтобы обеспечить строгую трехцветную инвариантность, но среда выполнения не позволяла вставлять барьеры записи для всех корневых объектов, удаленных сборщиком мусора. Поскольку приложения на языке Go могут содержать сотни или тысячи горутин, а корневые объекты сборки мусора обычно включают в себя глобальные переменные и объекты стека, если среде выполнения необходимо открыть барьеры записи в стеках сотен горутин, это приведет к огромным последствиям. накладные расходы, поэтому команда Go решила внедрить его, когда фаза маркировки была завершена.Приостановите программу, закрасьте все объекты стека серым цветом и повторите сканирование., в активном гурет Во многих программах процесс спансинга занимает от 10 до 100 мс.

Язык Go в версии 1.8 сочетает в себе барьер записи для вставки Дейкстры и барьер для удаления записи Юасы, образуя гибридный барьер записи, как показано ниже.Затенить перезаписанные объекты и затенить новые объекты, если текущий стек не сканируется:

writePointer(slot, ptr):
    shade(*slot)
    if current stack is grey:
        shade(ptr)
    *slot = ptr

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

7.2.3 Принципы реализации

Прежде чем представить эволюцию сборщика мусора, нам нужно иметь некоторое начальное представление о цикле выполнения последнего сборщика мусора, что является относительно полезным подспорьем в нашей глобальной конструкции. Сборку мусора языка Go можно разделить на четкую завершение, тег, завершение тега и очистку четырех разных этапов, каждый из которых выполняет свою работу соответственно.25:

garbage-collector-phases

Рисунок 7-35 Несколько этапов сборки мусора

  1. фаза завершения очистки;
    1. Приостановить программу, в это время все процессоры войдут в безопасную точку;
    2. Если текущий цикл сборки мусора срабатывает принудительно, нам также необходимо иметь дело с не очищенными блоками управления памятью;
  2. Отметить фазу;
    1. переключить состояние на_GCmark, включите барьер записи, поддержку программы пользователя (Mutator Assiste) и поставьте корневой объект в очередь;
    2. Возобновление выполнения, маркировка процессов и вспомогательные пользовательские программы начнут одновременно маркировать объекты в памяти, барьеры записи будут помечать как перезаписанные указатели, так и новые указатели серым цветом, а все вновь созданные объекты будут непосредственно помечаться черным цветом;
    3. Начать сканирование корневых объектов, включая все стеки Goroutine, глобальные объекты и структуры данных времени выполнения, которые не находятся в куче. Текущий процессор будет приостановлен во время сканирования стека Goroutine;
    4. Обрабатывайте объекты в серой очереди по очереди, помечая объекты черным цветом, а объекты, на которые они указывают, серым цветом;
    5. Используйте алгоритм распределенного завершения для проверки оставшейся работы и перейдите к этапу завершения тега после завершения этапа маркировки;
  3. отметить стадию завершения;
    1. Приостановить программу, переключите состояние на_GCmarkterminationи закрыть программу пользователя вспомогательного маркера;
    2. очистить кеш потоков на процессоре;
  4. этап очистки;
    1. переключить состояние на_GCoffЗапустите фазу очистки, инициализируйте состояние очистки и закройте барьер записи;
    2. Восстановите программу пользователя, все вновь созданные объекты будут отмечены белым цветом;
    3. Все единицы управления памятью очищаются одновременно в фоновом режиме, и очистка запускается, когда Goroutine применяет новую единицу управления памятью;

Пока среда выполнения будет использовать только_GCoff,_GCmarkа также_GCmarkterminationТри состояния представляют все этапы сборки мусора, но реализация намного сложнее.В этом разделе будут подробно проанализированы принципы реализации в соответствии с различными этапами сборки мусора.

глобальная переменная

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

  • runtime.gcphaseстадия, на которой в данный момент находится сборщик мусора, возможно,_GCoff,_GCmarkа также_GCmarktermination, Горутин Атомарность должна быть гарантирована при чтении или изменении этого этапа;
  • runtime.gcBlackenEnabled— логическое значение, которое устанавливается равным 1, когда сборка мусора находится в фазе маркировки, когда пользовательские программы, помогающие сборке мусора и задачам фоновой маркировки, могут затемнять объекты;
  • runtime.gcControllerРеализован алгоритм синхронизации сборки мусора, который определяет, когда запускается параллельная сборка мусора и какая работа должна быть обработана;
  • runtime.gcpercentЭто процент прироста памяти, который запускает сборку мусора, по умолчанию он равен 100, то есть GC должен срабатывать, когда память кучи увеличивается на 100% по сравнению с последней сборкой мусора, а параллельный сборщик мусора завершит сборку мусора до достижения цели;
  • runtime.writeBarrierпредставляет собой структуру, содержащую состояние барьера записи, гдеenabledПоле указывает на открытие и закрытие барьера записи;
  • runtime.worldsemaЭто глобальный семафор, и поток, получивший семафор, имеет право приостановить текущее приложение;

В дополнение к вышеуказанным глобальным переменным, нам также нужно кратко понять здесьruntime.workПеременная:

var work struct {
	full  lfstack
	empty lfstack
	pad0  cpu.CacheLinePad

	wbufSpans struct {
		lock mutex
		free mSpanList
		busy mSpanList
	}
	...
	nproc  uint32
	tstart int64
	nwait  uint32
	ndone  uint32
	...
	mode gcMode
	cycles uint32
	...
	stwprocs, maxprocs int32
	...
}

Эта структура содержит большое количество полей, связанных со сборкой мусора, таких как: количество завершенных циклов сборки мусора, текущее время цикла и загрузка ЦП, режим сборки мусора и т.д. Мы увидим эту структуру в следующих подразделах дополнительные поля в организме.

Время запуска

Среда выполнения пройдет следующееruntime.gcTrigger.testМетод решает, нужно ли запускать сборку мусора.При выполнении основных условий для запуска сборки мусора — сборка мусора разрешена, программа не падает и не находится в цикле сборки мусора, метод запускает различные проверки тремя разными способами:

func (t gcTrigger) test() bool {
	if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {
		return false
	}
	switch t.kind {
	case gcTriggerHeap:
		return memstats.heap_live >= memstats.gc_trigger
	case gcTriggerTime:
		if gcpercent < 0 {
			return false
		}
		lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
		return lastgc != 0 && t.now-lastgc > forcegcperiod
	case gcTriggerCycle:
		return int32(t.n-work.cycles) > 0
	}
	return true
}
  1. gcTriggerHeap— Выделение кучи памяти достигает триггерного размера кучи, рассчитанного контроллером;
  2. gcTriggerTime— При отсутствии триггера в течение определенного периода времени будет запущен новый цикл, а условие запуска определяетсяruntime.forcegcperiodПеременное управление, по умолчанию 2 минуты;
  3. gcTriggerCycle- Если текущая сборка мусора не включена, запускается новый цикл;

Метод, используемый для включения сборки мусораruntime.gcStartполучитruntime.gcTriggerтип предиката, мы можем запустить на основе этого_GCoffВыходящая структура находит код для всех запущенных сборок мусора:

  • runtime.sysmonа такжеruntime.forcegchelper— запуск периодических проверок и сбор мусора в фоновом режиме;
  • runtime.GC- Пользовательская программа запускает сборку мусора вручную;
  • runtime.mallocgc- запускать размер кучи сборки мусора в соответствии со временем памяти приложения;

garbage-collector-trigger

Рисунок 7-36 Триггер сборки мусора

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

фоновый триггер

Среда выполнения запустит горутину в фоновом режиме, когда приложение начнет принудительно запускать сборку мусора.Обязанности этой горутины очень просты — вызовruntime.gcStartМетод пытается начать новый раунд сборки мусора:

func init() {
	go forcegchelper()
}

func forcegchelper() {
	forcegc.g = getg()
	for {
		lock(&forcegc.lock)
		atomic.Store(&forcegc.idle, 1)
		goparkunlock(&forcegc.lock, waitReasonForceGGIdle, traceEvGoBlock, 1)
		gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()})
	}
}

Для того, чтобы уменьшить занятость вычислительных ресурсов, которые будут вызываться в цикле Goroutineruntime.goparkunlockАктивно засыпать и ждать пробуждения других горутин,runtime.forcegchelperСпит большую часть времени, но проверяется системным мониторомruntime.sysmonПросыпаться при выполнении условий сборки мусора:

func sysmon() {
	...
	for {
		...
		if t := (gcTrigger{kind: gcTriggerTime, now: now}); t.test() && atomic.Load(&forcegc.idle) != 0 {
			lock(&forcegc.lock)
			forcegc.idle = 0
			var list gList
			list.push(forcegc.g)
			injectglist(&list)
			unlock(&forcegc.lock)
		}
	}
}

Мониторинг системы активно строитruntime.gcTriggerИ проверьте, выполняется ли триггерное условие сборки мусора, если условие выполняется, системный мониторингruntime.forcegcГорутины, находящиеся в состоянии, присоединяются к глобальной очереди и ждут, пока планировщик запланирует выполнение.

Ручной триггер

Программа пользователя пройдетruntime.GCФункция активно уведомляет среду выполнения о выполнении во время работы программы.При вызове этого метода вызывающая сторона будет заблокирована до завершения текущего цикла сборки мусора.Во время сборки мусора вся программа также может быть приостановлена ​​через STW:

func GC() {
	n := atomic.Load(&work.cycles)
	gcWaitOnMark(n)
	gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1})
	gcWaitOnMark(n + 1)

	for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) {
		sweep.nbgsweep++
		Gosched()
	}

	for atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) != 0 {
		Gosched()
	}

	mp := acquirem()
	cycle := atomic.Load(&work.cycles)
	if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) {
		mProf_PostSweep()
	}
	releasem(mp)
}
  1. Прежде чем официально начать сборку мусора, среда выполнения должна пройтиruntime.gcWaitOnMarkФункция ожидает завершения фаз маркировки-завершения, маркировки-и-маркировки-завершения предыдущего цикла;
  2. передачаruntime.gcStartЗапустить новый раунд сбора мусора и пройтиruntime.gcWaitOnMarkПодождите, пока отмеченная фаза завершения этого раунда сборки мусора завершится нормально;
  3. Продолжай звонитьruntime.sweeponeОчищает все ожидающие единицы управления памятью и ожидает завершения всей работы по очистке, во время которой она будет вызванаruntime.Goschedотказаться от процессора;
  4. Завершив очистку этого раунда сборки мусора, передайтеruntime.mProf_PostSweepПубликуя снимок состояния кучи памяти на этом этапе, мы можем получить состояние памяти на данный момент;

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

запросить память

Последнее, что может вызвать сборку мусора, этоruntime.mallocgcФункция, которую мы представили в предыдущем разделе о распределителе памяти, что объекты в куче будут разделены на три типа в зависимости от размера: микрообъекты, маленькие объекты и большие объекты, Создание этих трех типов объектов может вызвать новый мусор Цикл сбора:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	shouldhelpgc := false
	...
	if size <= maxSmallSize {
		if noscan && size < maxTinySize {
			...
			v := nextFreeFast(span)
			if v == 0 {
				v, _, shouldhelpgc = c.nextFree(tinySpanClass)
			}
			...
		} else {
			...
			v := nextFreeFast(span)
			if v == 0 {
				v, span, shouldhelpgc = c.nextFree(spc)
			}
		  ...
		}
	} else {
		shouldhelpgc = true
		...
	}
	...
	if shouldhelpgc {
		if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
			gcStart(t)
		}
	}

	return x
}
  1. При отсутствии свободного места в блоке управления памятью текущего потока создание микрообъектов и мелких объектов требует вызоваruntime.mcache.nextFreeМетод получает новую единицу управления из центрального кэша или кучи страниц, после чего может быть запущена сборка мусора;
  2. Когда пользовательская программа запрашивает выделение большого объекта размером более 32 КБ, он будет созданruntime.gcTriggerСтруктура пытается инициировать сборку мусора;

Запуск сборки мусора через память кучи требует сравненияruntime.mstatsДва поля в — представляют количество байтов живых объектов в сборке мусора.heap_liveи размер кучи памяти, представляющий метку триггераgc_trigger; Когда количество байтов живых объектов в памяти превышает размер кучи, запускающий сборку мусора, запускается новый раунд сборки мусора. Здесь мы представим процесс расчета этих двух значений отдельно:

  1. heap_live— Для уменьшения конкуренции за блокировки среда выполнения будет обновляться только тогда, когда центральный кеш выделяет или освобождает блоки управления памятью и выделяет большие объекты в куче;
  2. gc_trigger— вызывается во время отмеченной фазы завершенияruntime.gcSetTriggerRatioОбновите размер кучи, который запускает следующую сборку мусора;

runtime.gcControllerКоэффициент срабатывания рассчитывается после каждого цикла и передаетсяruntime.gcSetTriggerRatioнастраиватьgc_trigger, он может определить время запуска сборки мусора и количество отмеченных задач, обрабатываемых пользовательской программой и фоном.Алгоритм контроля обратной связи используется для определения времени запуска сборки мусора в соответствии с ростом кучи и использованием ЦП сбора мусора.

ты сможешьruntime.gcControllerState.endCycleНайдите алгоритм стимуляции сборки мусора, предложенный v1.5, в методе26, И вruntime.gcControllerState.reviseНайдите алгоритм разделения целевых объектов мягкой и жесткой кучи, представленный в версии 1.10, в методе27.

Начинается сбор мусора

Сборка мусора должна вызываться во время запускаruntime.gcStartфункция, хотя реализация этой функции более сложная, но ее основная обязанность состоит в том, чтобы изменить глобальное состояние сборки мусора на_GCmarkИ проведите некоторую подготовительную работу, мы будем внедрять реализацию этой функции в следующие этапы:

  1. позвони дваждыruntime.gcTrigger.testМетод проверяет, выполняются ли условия сборки мусора;
  2. Приостановить программу, запустить работник Goroutine на заднем плане, чтобы обрабатывать задачу маркировки, убедитесь, что все блоки управления памятью очищены, а другие препараты до начала маркировки начнется;
  3. Войдите в фазу маркировки, подготовьтесь к работе в фоновом режиме, отметьте корневой объект и микрообъекты, восстановите программу пользователя и войдите в фазу одновременного сканирования и маркировки;

Этот метод также вызывается постоянно в цикле при проверке условия сборки мусора.runtime.sweeponeОчистить уже отмеченные ячейки памяти, завершив последний цикл сборки мусора:

func gcStart(trigger gcTrigger) {
	for trigger.test() && sweepone() != ^uintptr(0) {
		sweep.nbgsweep++
	}

	semacquire(&work.startSema)
	if !trigger.test() {
		semrelease(&work.startSema)
		return
	}
	...
}

После проверки условий сборки мусора и доработки метод проходитsemacquireстать глобальнымworldsemaсемафор, звонокruntime.gcBgMarkStartWorkersЗапустите задачу выделения фона, вызовите системный стекruntime.stopTheWorldWithSemaПриостановить программу и позвонитьruntime.finishsweep_mДля обеспечения нормальной утилизации предыдущего блока памяти:

func gcStart(trigger gcTrigger) {
	...
	semacquire(&worldsema)
	gcBgMarkStartWorkers()
	work.stwprocs, work.maxprocs = gomaxprocs, gomaxprocs
	...

	systemstack(stopTheWorldWithSema)
	systemstack(func() {
		finishsweep_m()
	})

	work.cycles++
	gcController.startCycle()
	...
}

В дополнение к этому, описанный выше процесс также изменяет глобальные переменные.runtime.workСостояние удерживается, включая количество горутин, необходимых для сборки мусора, и количество завершенных циклов.

После завершения всех приготовлений метод вступает в финальную стадию исполнения. На этом этапе мы изменяем глобальное состояние сборки мусора на_GCmarkи последовательно выполните следующие действия:

  1. передачаruntime.gcBgMarkPrepareФункция инициализирует состояние, требуемое фоновым сканированием;
  2. передачаruntime.gcMarkRootPrepareФункция сканирует стек, глобальные переменные и другие корневые объекты и добавляет их в очередь;
  3. установить глобальную переменнуюruntime.gcBlackenEnabled, пользовательские программы и задачи маркировки могут затемнять объекты;
  4. передачаruntime.startTheWorldWithSemaЗапустите программу, фоновая задача также начнет маркировать объекты в куче;
func gcStart(trigger gcTrigger) {
	...
	setGCPhase(_GCmark)

	gcBgMarkPrepare()
	gcMarkRootPrepare()

	atomic.Store(&gcBlackenEnabled, 1)
	systemstack(func() {
		now = startTheWorldWithSema(trace.enabled)
		work.pauseNS += now - work.pauseStart
		work.tMark = now
	})
	semrelease(&work.startSema)
}

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

Пауза и возобновление программы

runtime.stopTheWorldWithSemaа такжеruntime.startTheWorldWithSemaЭто пара основных функций для приостановки и возобновления программ.У них совершенно противоположные функции, но приостановка программы сложнее, чем возобновление.Посмотрим на принцип реализации первой:

func stopTheWorldWithSema() {
	_g_ := getg()
	sched.stopwait = gomaxprocs
	atomic.Store(&sched.gcwaiting, 1)
	preemptall()
	_g_.m.p.ptr().status = _Pgcstop
	sched.stopwait--
	for _, p := range allp {
		s := p.status
		if s == _Psyscall && atomic.Cas(&p.status, s, _Pgcstop) {
			p.syscalltick++
			sched.stopwait--
		}
	}
	for {
		p := pidleget()
		if p == nil {
			break
		}
		p.status = _Pgcstop
		sched.stopwait--
	}
	wait := sched.stopwait > 0
	if wait {
		for {
			if notetsleep(&sched.stopnote, 100*1000) {
				noteclear(&sched.stopnote)
				break
			}
			preemptall()
		}
	}
}

Процедура паузы в основном используетruntime.preemptallфункция, которая вызываетruntime.preemptone, так как максимальное количество активных процессов в программе равноgomaxprocs,такruntime.stopTheWorldWithSemaЭта переменная уменьшается каждый раз, когда обнаруживается остановившийся процессор, пока не будут остановлены все процессоры. Эта функция будет последовательно останавливать текущий процессор, ожидать процессора в системном вызове, а также захватывать и вытеснять бездействующий процессор.Состояние процессора будет обновлено до_Pgcstop, ожидая, пока сборщик мусора снова проснется.

В процессе восстановления программы используетсяruntime.startTheWorldWithSema, реализация этой функции относительно проста:

  1. передачаruntime.netpollПолучить ожидающие задачи из сетевого опросника и присоединиться к глобальной очереди;
  2. передачаruntime.procresizeРасширить или уменьшить глобальный процессор;
  3. передачаruntime.notewakeupилиruntime.newmРазбудите процессор по очереди или создайте новый поток для процессора;
  4. Если количество ожидающих выполнения горутин слишком велико, создайте дополнительные процессоры, чтобы помочь в выполнении задачи;
func startTheWorldWithSema(emitTraceEvent bool) int64 {
	mp := acquirem()
	if netpollinited() {
		list := netpoll(0)
		injectglist(&list)
	}

	procs := gomaxprocs
	p1 := procresize(procs)
	sched.gcwaiting = 0
	...
	for p1 != nil {
		p := p1
		p1 = p1.link.ptr()
		if p.m != 0 {
			mp := p.m.ptr()
			p.m = 0
			mp.nextp.set(p)
			notewakeup(&mp.park)
		} else {
			newm(nil, p)
		}
	}

	if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 {
		wakep()
	}
	...
}

Процесс приостановки и запуска программы относительно прост, и программа приостановки будет использоватьruntime.preemptallВыгрузить все процессоры, которые будут использоваться при возобновлении работы программыruntime.notewakeupилиruntime.newmРазбудить процессор в программе.

Режим фонового маркера

Во время запуска сборки мусора среда выполнения вызываетruntime.gcBgMarkStartWorkersСоздайте горутины для задач фоновой маркировки для каждого процессора глобально, каждая из которых будет работатьruntime.gcBgMarkWorker, все бегутruntime.gcBgMarkWorkerГорутины засыпают после запуска и ждут, пока проснется планировщик:

func gcBgMarkStartWorkers() {
	for _, p := range allp {
		if p.gcBgMarkWorker == 0 {
			go gcBgMarkWorker(p)
			notetsleepg(&work.bgMarkReady, -1)
			noteclear(&work.bgMarkReady)
		}
	}
}

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

p-and-bg-mark-worker

Рисунок 7-37 Процессор и задача фоновой маркировки

Планировщик планирует циклruntime.scheduleТакже доступно через контроллер сборки мусораruntime.gcControllerState.findRunnabledGCWorkerМетод получает и выполняет задачу фоновой маркировки.

Всего существует три различных режима рабочих горутин для одновременного сканирования объектов.runtime.gcMarkWorkerMode, эти три разных режима горутин используют совершенно разные стратегии при маркировке объектов, и контроллер сборки мусора будет выполнять различные типы рабочих сопрограмм по мере необходимости:

  • gcMarkWorkerDedicatedMode- Процессор несет исключительную ответственность за маркировку объектов и не будет вытеснен планировщиком;
  • gcMarkWorkerFractionalMode— Когда фоновое использование ЦП для сборки мусора не соответствует ожидаемому (по умолчанию — 25%), запустите этот тип рабочей сопрограммы, чтобы помочь сборке мусора достичь целевого использования, поскольку она занимает только часть ресурсов того же ЦП, поэтому это может быть запланировано;
  • gcMarkWorkerIdleMode- Когда процессор не выполняет Goroutine, он запускает задачу миссии сбора мусора до тех пор, пока она не будет искренне;

runtime.gcControllerState.startCycleВышеупомянутое будет рассчитываться на основе количества глобальных процессоров и использования ЦП при сборке мусора.dedicatedMarkWorkersNeededа такжеfractionalUtilizationGoalопределить количество рабочих сопрограмм в разных режимах.

Поскольку загрузка ЦП задачи фоновой маркировки составляет 25%, если хост 4-ядерный или 8-ядерный, то для сборки мусора требуется 1 или 2 горутины, предназначенные для обработки связанных задач; однако, если хост 3-ядерный или 6-ядерный -core, потому что оно не может быть 4 делится, поэтому в это время требуется 0 или 1 Горутина, предназначенная для сборки мусора, и она должна занимать часть времени определенного ЦП при работе.gcMarkWorkerFractionalModeСопрограммы режима гарантируют загрузку ЦП.

cpu-number-and-gc-mark-worker-mode

Рис. 7-38 Количество ядер хоста и режим задачи сборки мусора

Контроллер сборки мусора будетruntime.gcControllerState.findRunnabledGCWorkerметод установки обработчикаgcMarkWorkerMode:

func (c *gcControllerState) findRunnableGCWorker(_p_ *p) *g {
	...
	if decIfPositive(&c.dedicatedMarkWorkersNeeded) {
		_p_.gcMarkWorkerMode = gcMarkWorkerDedicatedMode
	} else if c.fractionalUtilizationGoal == 0 {
		return nil
	} else {
		delta := nanotime() - gcController.markStartTime
		if delta > 0 && float64(_p_.gcFractionalMarkTime)/float64(delta) > c.fractionalUtilizationGoal {
			return nil
		}
		_p_.gcMarkWorkerMode = gcMarkWorkerFractionalMode
	}

	gp := _p_.gcBgMarkWorker.ptr()
	casgstatus(gp, _Gwaiting, _Grunnable)
	return gp
}

Реализация вышеуказанного метода относительно понятна, контроллер проходитdedicatedMarkWorkersNeededОпределите количество горутин, выделенных для выполнения отмеченной задачи, и решите, следует ли начинать, исходя из времени и общего времени выполнения отмеченной задачи.gcMarkWorkerFractionalModeрежим Goroutine; в дополнение к рабочим сопрограммам, необходимым для этих двух контроллеров, планировщикruntime.findrunnableФункция использует простаивающие процессоры для выполнения сборки мусора, чтобы ускорить процесс:

func findrunnable() (gp *g, inheritTime bool) {
	...
stop:
	if gcBlackenEnabled != 0 && _p_.gcBgMarkWorker != 0 && gcMarkWorkAvailable(_p_) {
		_p_.gcMarkWorkerMode = gcMarkWorkerIdleMode
		gp := _p_.gcBgMarkWorker.ptr()
		casgstatus(gp, _Gwaiting, _Grunnable)
		return gp, false
	}
	...
}

Три разных режима рабочих сопрограмм взаимодействуют друг с другом, чтобы гарантировать, что использование ЦП при сборке мусора достигает желаемого порога, а задача маркировки завершается до достижения целевого размера кучи.

Параллельное сканирование и помощник по отметке

runtime.gcBgMarkWorkerЭто функция, выполняемая задачей маркировки в фоновом режиме. Сканирование и маркировка графа объектов в памяти выполняются в цикле этой функции. Мы представляем принцип реализации этой функции в трех частях:

  1. Получите текущий процессор и горутину, упакованную вparkInfoтип строения и активно засыпают в ожидании пробуждения;
  2. По процессоруgcMarkWorkerModeРежим определяет стратегию сканирования задач;
  3. После того, как все задачи завершены отметку, позвонитеruntime.gcMarkDoneМетод завершает этап маркировки;

Во-первых, давайте посмотрим на подготовку фоновой задачи тегирования.Среда выполнения создаетparkInfoСтруктура, которая предварительно сохраняет процессор и текущую горутину, когда мы вызываемruntime.goparkПри активации гибернации среда выполнения безопасно установит связь между процессором и задачей фонового маркера в системном стеке:

func gcBgMarkWorker(_p_ *p) {
	gp := getg()

	type parkInfo struct {
		m      muintptr
		attach puintptr
	}

	park := new(parkInfo)
	park.m.set(acquirem())
	park.attach.set(_p_)
	notewakeup(&work.bgMarkReady)

	for {
		gopark(func(g *g, parkp unsafe.Pointer) bool {
			park := (*parkInfo)(parkp)
			releasem(park.m.ptr())

			if park.attach != 0 {
				p := park.attach.ptr()
				park.attach.set(nil)
				if !p.gcBgMarkWorker.cas(0, guintptr(unsafe.Pointer(g))) {
					return false
				}
			}
			return true
		}, unsafe.Pointer(park), waitReasonGCWorkerIdle, traceEvGoBlock, 0)
	...
}

пройти черезruntime.goparkБездействующая горутина не войдет в очередь выполнения, она будет ждать только прямого пробуждения контроллера сборки мусора или планировщика; после пробуждения мыgcMarkWorkerModeВыберите разные стратегии выполнения тегов, будут вызываться разные стратегии выполнения.runtime.gcDrainСканировать рабочий буферruntime.gcWork:

		if _p_.gcBgMarkWorker.ptr() != gp {
			break
		}
		park.m.set(acquirem())

		atomic.Xadd(&work.nwait, -1)
		systemstack(func() {
			casgstatus(gp, _Grunning, _Gwaiting)
			switch _p_.gcMarkWorkerMode {
			case gcMarkWorkerDedicatedMode:
				gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
				if gp.preempt {
					lock(&sched.lock)
					for {
						gp, _ := runqget(_p_)
						if gp == nil {
							break
						}
						globrunqput(gp)
					}
					unlock(&sched.lock)
				}
				gcDrain(&_p_.gcw, gcDrainFlushBgCredit)
			case gcMarkWorkerFractionalMode:
				gcDrain(&_p_.gcw, gcDrainFractional|gcDrainUntilPreempt|gcDrainFlushBgCredit)
			case gcMarkWorkerIdleMode:
				gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
			}
			casgstatus(gp, _Gwaiting, _Grunning)
		})
		incnwait := atomic.Xadd(&work.nwait, +1)

должны знать о том,gcMarkWorkerDedicatedModeЗадачи режима не могут быть вытеснены. Чтобы уменьшить дополнительные накладные расходы, первый вызовruntime.gcDrainразрешено вытеснение, но как только процессор вытеснен, текущий Горутина перенесет все исполняемые горутины на процессоре в глобальную очередь, чтобы обеспечить занятость ресурсов ЦП сборкой мусора. Когда все фоновые рабочие задачи застряли в ожидании и работы не осталось, мы считаем, что фаза маркировки этого раунда сборки мусора завершена, и мы вызываемruntime.gcMarkDoneфункция:

		if incnwait == work.nproc && !gcMarkWorkAvailable(nil) {
			_p_.gcBgMarkWorker.set(nil)
			releasem(park.m.ptr())
			gcMarkDone()
			park.m.set(acquirem())
			park.attach.set(_p_)
		}
	}
}

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

Рабочий пул

вызовruntime.gcDrainфункция, время выполнения будет проходить вruntime.gcWork, эта структура является абстракцией рабочего пула в сборщике мусора. Она реализует модель производителя и потребителя. Мы можем использовать эту структуру в качестве отправной точки для понимания отмеченной работы в целом:

gc-work-pool

Рисунок 7-39 Рабочий пул сборщика мусора

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

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

global-work-and-local-work

Рисунок 7-40 Глобальная задача и локальная задача

runtime.gcWork.balanceМетод поставит часть локальной работы процессора обратно в глобальную очередь, и пусть ее обрабатывают другие процессоры, чтобы обеспечить баланс загрузки разных процессоров.

runtime.gcWorkОбеспечивает абстракцию задач производства и потребления для сборщика мусора, эта структура содержит два важных рабочих буфера.wbuf1а такжеwbuf2, два буфера являются основным буфером и резервным буфером:

type gcWork struct {
	wbuf1, wbuf2 *workbuf
	...
}

type workbufhdr struct {
	node lfnode // must be first
	nobj int
}

type workbuf struct {
	workbufhdr
	obj [(_WorkbufSize - unsafe.Sizeof(workbufhdr{})) / sys.PtrSize]uintptr
}

Когда мы добавляем или удаляем объекты в структуру, она всегда сначала оперирует основным буфером, когда места в основном буфере недостаточно или нет объекта, срабатывает переключение основного и резервного буферов, а когда оба буфера заполнены Когда он недостаточен или пуст, объект будет вставлен или получен из глобального рабочего буфера.Реализация родственных методов структуры очень проста, и анализ здесь проводиться не будет.

объект сканирования

Это будет использовать время выполненияruntime.gcDrainФункция сканирует рабочий буфер на наличие серых объектов,gcDrainFlagsРазличные варианты различных стратегий:

func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	gp := getg().m.curg
	preemptible := flags&gcDrainUntilPreempt != 0
	flushBgCredit := flags&gcDrainFlushBgCredit != 0
	idle := flags&gcDrainIdle != 0

	initScanWork := gcw.scanWork
	checkWork := int64(1<<63 - 1)
	var check func() bool
	if flags&(gcDrainIdle|gcDrainFractional) != 0 {
		checkWork = initScanWork + drainCheckThreshold
		if idle {
			check = pollWork
		} else if flags&gcDrainFractional != 0 {
			check = pollFractionalWorkerExit
		}
	}
	...
}
  • gcDrainUntilPreempt— Когда ГорутинpreemptВозвращается, когда для поля установлено значение true;
  • gcDrainIdle- передачаruntime.pollWorkФункция, которая возвращается, когда на процессоре должны быть выполнены другие горутины;
  • gcDrainFractional- передачаruntime.pollFractionalWorkerExitфункция, когда загрузка ЦП превышаетfractionalUtilizationGoal20% от возврата;
  • gcDrainFlushBgCredit- передачаruntime.gcFlushBgCreditРассчитайте количество задач маркировки, выполняемых в фоновом режиме, чтобы уменьшить нагрузку на пользовательские программы, помогающие сборке мусора во время параллельной маркировки;

Среда выполнения будет использовать локальную переменнуюcheckФункция проверяет, следует ли в данный момент завершить задачу маркировки и передать обработчик. Когда мы закончим с подготовительной работой, мы можем начать сканирование корневого объекта в глобальной переменной, что также является первой задачей, которую необходимо выполнить на этапе маркировки:

func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	...
	if work.markrootNext < work.markrootJobs {
		for !(preemptible && gp.preempt) {
			job := atomic.Xadd(&work.markrootNext, +1) - 1
			if job >= work.markrootJobs {
				break
			}
			markroot(gcw, job)
			if check != nil && check() {
				goto done
			}
		}
	}
	...
}

Для сканирования корневых объектов необходимо использоватьruntime.markrootФункция, которая сканирует кэш, сегмент данных, сегмент BSS, где хранятся глобальные и статические переменные, а также падающая память стека героутина; как только сканирование корневого объекта завершено, текущий Goroutine начнет получать ожидающие данные от локального и глобальные рабочие буферные пулы. Задачи выполнены:

func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	...
	for !(preemptible && gp.preempt) {
		if work.full == 0 {
			gcw.balance()
		}

		b := gcw.tryGetFast()
		if b == 0 {
			b = gcw.tryGet()
			if b == 0 {
				wbBufFlush(nil, 0)
				b = gcw.tryGet()
			}
		}
		if b == 0 {
			break
		}
		scanobject(b, gcw)

		if gcw.scanWork >= gcCreditSlack {
			atomic.Xaddint64(&gcController.scanWork, gcw.scanWork)
			if flushBgCredit {
				gcFlushBgCredit(gcw.scanWork - initScanWork)
				initScanWork = 0
			}
			checkWork -= gcw.scanWork
			gcw.scanWork = 0

			if checkWork <= 0 {
				checkWork += drainCheckThreshold
				if check != nil && check() {
					break
				}
			}
		}
	}
	...
}

Отсканированные объекты будут использоватьruntime.scanobject, функция начнет сканирование с переданной позиции и будет вызываться во время сканированияruntime.greyobjectРаскрасьте найденные активные объекты.

func gcDrain(gcw *gcWork, flags gcDrainFlags) {
	...
done:
	if gcw.scanWork > 0 {
		atomic.Xaddint64(&gcController.scanWork, gcw.scanWork)
		if flushBgCredit {
			gcFlushBgCredit(gcw.scanWork - initScanWork)
		}
		gcw.scanWork = 0
	}
}

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

Процесс сканирования и маркировки объектов в памяти включает в себя множество битовых операций и операций с указателями, а реализация соответствующего кода относительно сложна. Мы не будем вводить здесь соответствующее содержание. Заинтересованные читатели могут обратиться кruntime.gcDrainВ качестве входа для изучения специфики процесса трехцветной маркировки.

написать барьер

Барьер записи — это технология, обеспечивающая безопасность и недостижимость одновременной маркировки в языке Go. Нам необходимо использовать смешанные барьеры записи, чтобы поддерживать слабую трехцветную инвариантность графов объектов. Однако реализация барьеров записи требует сотрудничества компилятора и среды выполнения. . На этапе генерации промежуточного кода SSA компилятор используетcmd/compile/internal/ssa.writebarrierфункционировать вStore,Moveа такжеZeroДобавьте к операции барьер записи, чтобы сгенерировать следующий код:

if writeBarrier.enabled {
  gcWriteBarrier(ptr, val)
} else {
  *ptr = val
}

Когда язык Go входит в фазу сборки мусора, глобальные переменныеruntime.writeBarrierсерединаenabledПоле будет включено, и все операции записи будут вызыватьruntime.gcWriteBarrier:

TEXT runtime·gcWriteBarrier(SB),NOSPLIT,$28
	...
	get_tls(BX)
	MOVL	g(BX), BX
	MOVL	g_m(BX), BX
	MOVL	m_p(BX), BX
	MOVL	(p_wbBuf+wbBuf_next)(BX), CX
	LEAL	8(CX), CX
	MOVL	CX, (p_wbBuf+wbBuf_next)(BX)
	CMPL	CX, (p_wbBuf+wbBuf_end)(BX)
	MOVL	AX, -8(CX)	// 记录值
	MOVL	(DI), BX
	MOVL	BX, -4(CX)	// 记录 *slot
	JEQ	flush
ret:
	MOVL	20(SP), CX
	MOVL	24(SP), BX
	MOVL	AX, (DI) // 触发写操作
	RET

flush:
  ...
	CALL	runtime·wbBufFlush(SB)
  ...
	JMP	ret

В приведенной выше ассемблерной функции регистр DI является адресом назначения операции записи, а перезаписанное значение сохраняется в регистре AX.Эта функция перезапишет исходное значение и передастruntime.wbBufFlushСообщите сборщику мусора, чтобы он добавил исходное значение и новое значение в рабочую очередь текущего процессора.Поскольку реализация барьера записи сложна, барьер записи по-прежнему оказывает относительно большое влияние на производительность программы.Ранее, Для выполнения работы нужна была всего одна инструкция, теперь требуются десятки инструкций.

Мы упоминали выше, что смешанный барьер записи, состоящий из барьера записи Дейкстры и Юасы, включен, все вновь созданные объекты должны быть непосредственно окрашены в черный цвет.Процесс маркировки здесь состоит изruntime.gcmarknewobjectЗавершенный:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	if gcphase != _GCoff {
		gcmarknewobject(uintptr(x), size, scanSize)
	}
	...
}

func gcmarknewobject(obj, size, scanSize uintptr) {
	markBitsForAddr(obj).setMarked()
	gcw := &getg().m.p.ptr().gcw
	gcw.bytesMarked += uint64(size)
	gcw.scanWork += int64(scanSize)
}

runtime.mallocgcЭта функция будет вызываться после начала сборки мусора для получения блока памяти и бита флага, соответствующего объекту.runtime.markBitsи позвониruntime.markBits.setMarkedПросто покрасьте новый объект в черный цвет.

отметить помощь

Чтобы скорость выделения памяти пользовательской программы не превышала скорость маркировки фоновой задачи, среда выполнения также вводит технологию помощи в маркировке, которая следует очень простому и незатейливому принципу,Сколько задач по маркировке нужно выполнить, сколько выделено памяти. Каждая горутина содержитgcAssistBytesполе, в котором хранится количество байтов объекта вспомогательного токена текущей горутины. Во время фазы параллельной маркировки, когда горутина вызываетruntime.mallocgcПри выделении нового объекта функция проверяет, сводит ли концы с концами горутина, запросившая память:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	...
	var assistG *g
	if gcBlackenEnabled != 0 {
		assistG = getg()
		if assistG.m.curg != nil {
			assistG = assistG.m.curg
		}
		assistG.gcAssistBytes -= int64(size)

		if assistG.gcAssistBytes < 0 {
			gcAssistAlloc(assistG)
		}
	}
	...
	return x
}

Вызывается при выделении памятиruntime.gcAssistAllocи вызывается при сканировании памятиruntime.gcFlushBgCreditОни отвечают за "заимствование" и "погашение долгов" соответственно. С помощью этой системы управления долгом мы можем гарантировать, что горутины работают нормально, не вызывая слишком большого давления на сборку мусора, и гарантировать, что фаза маркировки будет завершена, когда целевой размер кучи будет достигнут. достиг.

gc-mutator-assist

Рисунок 7-41 Динамический баланс вспомогательных маркеров

удерживаемый каждой горутинойgcAssistBytesУказывает количество байтов текущего вспомогательного флага сопрограммы, удерживаемого глобальным контроллером сборки мусора.bgScanCreditУказывает количество байтов вспомогательного флага фоновой сопрограммы.Когда локальная горутина выделяет больше объектов, можно использовать общедоступный кредитbgScanCreditпогашать. Давайте сначала проанализируемruntime.gcAssistAllocРеализация функции:

func gcAssistAlloc(gp *g) {
	...
retry:
	debtBytes := -gp.gcAssistBytes
	scanWork := int64(gcController.assistWorkPerByte * float64(debtBytes))
	if scanWork < gcOverAssistWork {
		scanWork = gcOverAssistWork
		debtBytes = int64(gcController.assistBytesPerWork * float64(scanWork))
	}

	bgScanCredit := atomic.Loadint64(&gcController.bgScanCredit)
	stolen := int64(0)
	if bgScanCredit > 0 {
		if bgScanCredit < scanWork {
			stolen = bgScanCredit
			gp.gcAssistBytes += 1 + int64(gcController.assistBytesPerWork*float64(stolen))
		} else {
			stolen = scanWork
			gp.gcAssistBytes += debtBytes
		}
		atomic.Xaddint64(&gcController.bgScanCredit, -stolen)
		scanWork -= stolen

		if scanWork == 0 {
			return
		}
	}
	...
}

Функция сначала будет основываться на ГорутинеgcAssistBytesи конфигурация контроллера сборки мусора для расчета количества задач маркировки, которые необходимо выполнить, если глобальный кредитbgScanCreditЕсли есть доступные баллы, то этот балл будет вычтен.Поскольку нет блокировки для параллельного выполнения, глобальный кредит может быть обновлен до отрицательного значения, но в долгосрочной перспективе это не более важный вопрос.

Если глобального кредита недостаточно для покрытия локального долга, среда выполнения вызовет системный стек.runtime.gcAssistAlloc1Выполните задачу маркировки, функция будет вызвана напрямуюruntime.gcDrainNВыполните указанное количество отмеченных задач и верните:

func gcAssistAlloc(gp *g) {
	...
	systemstack(func() {
		gcAssistAlloc1(gp, scanWork)
	})
	...
	if gp.gcAssistBytes < 0 {
		if gp.preempt {
			Gosched()
			goto retry
		}
		if !gcParkAssist() {
			goto retry
		}
	}
}

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

для погашения долгаruntime.gcFlushBgCreditРеализация относительно проста: если во вспомогательной очереди нет ожидающей горутины, текущий кредит будет напрямую добавлен к глобальному кредиту.bgScanCreditсередина:

func gcFlushBgCredit(scanWork int64) {
	if work.assistQueue.q.empty() {
		atomic.Xaddint64(&gcController.bgScanCredit, scanWork)
		return
	}

	scanBytes := int64(float64(scanWork) * gcController.assistBytesPerWork)
	for !work.assistQueue.q.empty() && scanBytes > 0 {
		gp := work.assistQueue.q.pop()
		if scanBytes+gp.gcAssistBytes >= 0 {
			scanBytes += gp.gcAssistBytes
			gp.gcAssistBytes = 0
			ready(gp, 0, false)
		} else {
			gp.gcAssistBytes += scanBytes
			scanBytes = 0
			work.assistQueue.q.pushBack(gp)
			break
		}
	}

	if scanBytes > 0 {
		scanWork = int64(float64(scanBytes) * gcController.assistWorkPerByte)
		atomic.Xaddint64(&gcController.bgScanCredit, scanWork)
	}
}

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

global-credit-and-assist-bytes

Рисунок 7-42 Глобальный кредит и местный кредит

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

отмечать завершение

Когда все локальные задачи процессора завершены и не осталось рабочих горутин, будет вызвана фоновая параллельная задача или вспомогательная помеченная пользовательская программа.runtime.gcMarkDoneСообщите сборщику мусора. Когда все достижимые объекты помечены, эта функция переключает состояние сборки мусора в_GCmarktermination; если в локальной очереди все еще есть отложенные задачи, текущий метод добавит все задачи в глобальную очередь и будет ждать других задач Горутина завершает обработку:

func gcMarkDone() {
	...
top:
	if !(gcphase == _GCmark && work.nwait == work.nproc && !gcMarkWorkAvailable(nil)) {
		return
	}

	gcMarkDoneFlushed = 0
	systemstack(func() {
		gp := getg().m.curg
		casgstatus(gp, _Grunning, _Gwaiting)
		forEachP(func(_p_ *p) {
			wbBufFlush1(_p_)
			_p_.gcw.dispose()
			if _p_.gcw.flushedWork {
				atomic.Xadd(&gcMarkDoneFlushed, 1)
				_p_.gcw.flushedWork = false
			}
		})
		casgstatus(gp, _Gwaiting, _Grunning)
	})

	if gcMarkDoneFlushed != 0 {
		goto top
	}
	...
}

Если среда выполнения не содержит глобальных задач и в процессоре нет локальных задач, то серые объекты в текущем цикле сборки мусора помечаются черным цветом, и мы можем запустить фазовую миграцию, запускающую сборку мусора:

func gcMarkDone() {
	...
	getg().m.preemptoff = "gcing"
	systemstack(stopTheWorldWithSema)

	...

	atomic.Store(&gcBlackenEnabled, 0)
	gcWakeAllAssists()
	schedEnableUser(true)
	nextTriggerRatio := gcController.endCycle()
	gcMarkTermination(nextTriggerRatio)
}

Вышеупомянутые функции закроют смешанный барьер записи в конце, разбудят все пользовательские программы, которые помогают в сборке мусора, восстановят планирование пользовательских горутин и вызовутruntime.gcMarkTerminationВойдите в фазу завершения отметки:

func gcMarkTermination(nextTriggerRatio float64) {
	atomic.Store(&gcBlackenEnabled, 0)
	setGCPhase(_GCmarktermination)

	_g_ := getg()
	gp := _g_.m.curg
	casgstatus(gp, _Grunning, _Gwaiting)

	systemstack(func() {
		gcMark(startTime)
	})
	systemstack(func() {
		setGCPhase(_GCoff)
		gcSweep(work.mode)
	})
	casgstatus(gp, _Gwaiting, _Grunning)
	gcSetTriggerRatio(nextTriggerRatio)
	wakeScavenger()

	...

	injectglist(&work.sweepWaiters.list)
	systemstack(func() { startTheWorldWithSema(true) })
	prepareFreeWorkbufs()
	systemstack(freeStackSpans)
	systemstack(func() {
		forEachP(func(_p_ *p) {
			_p_.mcache.prepareForSweep()
		})
	})
	...
}

Мы опускаем код для многих статистических данных в функции подсчета строк, включая объем используемой памяти, время паузы текущего раунда сборки мусора и загрузку ЦП.Эти данные могут помочь контроллеру определить следующий раунд сборки мусора. триггеры Размер кучи, помимо статистики данных, эта функция также будет вызыватьruntime.gcSweepСбросить соответствующее состояние фазы очистки и заблокировать очистку всех блоков управления памятью, когда это необходимо;_GCmarkterminationСостояние недолго сохраняется при сборке мусора, оно быстро переходит в_GCoffИ восстановите приложение, весь процесс сборки мусора в основном здесь закончен, и пользовательская программа будет лениво освобождать память, когда она запрашивает память.

очистка памяти

Очистка сборки мусора включает сборщик объектов (Reclaimer) и сборщик блоков памяти, которые используют разные алгоритмы для очистки памяти кучи:

  • Сборщик объектов ищет и освобождает нетегированные объекты в блоке управления памятью, но еслиruntime.mspanВсе объекты вruntime.mcentral.cacheSpanилиruntime.sweeponeАсинхронный триггер;
  • Сборщик блоков памяти будет искать в памяти все объекты, которые не помечены.runtime.mspan, процесс будетruntime.mheap.reclaimвызывать;

runtime.sweeponeэто функция, которую мы часто видим во время сборки мусора, которая ищет единицы управления памятью, которые нужно очистить в памяти кучи:

func sweepone() uintptr {
	...
	var s *mspan
	sg := mheap_.sweepgen
	for {
		s = mheap_.sweepSpans[1-sg/2%2].pop()
		if s == nil {
			break
		}
		if state := s.state.get(); state != mSpanInUse {
			continue
		}
		if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
			break
		}
	}

	npages := ^uintptr(0)
	if s != nil {
		npages = s.npages
		if s.sweep(false) {
			atomic.Xadduintptr(&mheap_.reclaimCredit, npages)
		} else {
			npages = 0
		}
	}

	_g_.m.locks--
	return npages
}

При поиске модуля управления памятью он пройдетstateа такжеsweepgenДва поля определяют, нужно ли обрабатывать текущий блок. Если ячейка памятиsweepgenравныйmheap.sweepgen - 2, то это означает, что текущая ячейка нуждается в очистке, если равноmheap.sweepgen - 1, текущая оснастка очищается.

Вся переработка в конечном итоге осуществляетсяruntime.mspan.sweepЗавершено, функция восстанавливает мусор в ячейке памяти и очищает отметку, чтобы избежать следующего раунда сбора мусора на основе параллельной маркировки.

7.2.4 Сводка

Реализация сборщика мусора на языке Go очень сложна. Автор считает, что это самый сложный модуль в языке программирования. Сложность планировщика не на том же уровне, что и у сборщика мусора. Приходится многое опускать в процесс анализа сборщика мусора. Детали реализации , включая процесс одновременной маркировки объектов и конкретную реализацию очистки мусора, эти процессы проектируют большое количество низкоуровневых битовых операций и операций с указателями. Этот раздел содержит ссылки на все связанные коды, и заинтересованные читатели могут изучить их самостоятельно.

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

7.2.5 Дополнительная литература


  1. «Сборщики мусора — последовательный, параллельный, CMS, G1 (и что нового в Java 8)»blog.over о PS.com/garbage-col… ↩︎

  2. «Обзор управления памятью в Rust»ПК Уолтон.GitHub.IO/2013/03/18/… ↩︎

  3. «Автоматический подсчет ссылок (ARC) Objective-C»Surf.lilumu.org/docs/auto… ↩︎

  4. «Трехцветная маркировка»Итак, Wikipedia.org/wiki/Трейси, ты… ↩︎

  5. "Висячий указатель"En. Wikipedia.org/wiki/dang… ↩︎

  6. «Википедия: Барьер памяти»En. Wikipedia.org/wiki/memory… ↩︎

  7. П. П. Пиринен, Барьерные методы для инкрементной трассировки, Извещения ACM SIGPLAN, 34(3), 20–25, октябрь 1998 г.Войдите на .ACM.org/do i/10.1145… ↩︎

  8. Дейкстра Э. В., Лэмпорт Л., Мартин А. Дж., Шолтен К. С., Стеффенс Э. Ф. Оперативная сборка мусора: упражнение в сотрудничестве, Коммюнике ACM, 21(11), 966–975, 1978.Woohoo. В это время. u Texas. quota/users/E WD/he… ↩︎

  9. Юаса Т. Сборка мусора в реальном времени на машинах общего назначения Journal of Systems and Software, 11(3):181–198, 1990.woohoo.science direct.com/science/art… ↩︎

  10. Пол Р. Уилсон, «Методы сборки мусора для однопроцессорных систем».Woohoo. В это время. Растения. Квота/~Счет/курсы… ↩︎

  11. Performance · Go 1.1 Release Notes Go Waves .org / doc / go1.1 # боится ... ↩︎

  12. Changes to the garbage collector · Go 1.3 Release Notes golang.org/doc/go1.3#… ↩︎

  13. Go 1.5 concurrent garbage collector pacing docs.Google.com/document//… ↩︎

  14. Остин Клементс, 2015. «Предложение: плотные биты меток и распределение без развертки»go.google source.com/proposal/+/... ↩︎

  15. runtime: shrinkstack during mark termination significantly increases GC STW time GitHub.com/golang/go/i… ↩︎

  16. Proposal: Eliminate STW stack re-scanning go.Googlesource.com/proposal/+/… ↩︎

  17. Proposal: Eliminate STW stack re-scanning go.Googlesource.com/proposal/+/… ↩︎

  18. Proposal: Separate soft and hard heap size goal go.Googlesource.com/proposal/+/… ↩︎

  19. Proposal: Simplify mark termination and eliminate mark 2 ttps://go.googlesource.com/proposal/+/master/design/26903-simplify-mark-termination.md ↩︎

  20. Proposal: Smarter Scavenging go.Googlesource.com/proposal/+/… ↩︎

  21. Proposal: Scaling the Go page allocator go.Googlesource.com/proposal/+/… ↩︎

  22. Остин Клементс, 2015 г. «Предложение: децентрализованная координация GC».go.Googlesource.com/proposal/+/… ↩︎

  23. Go 1.5 concurrent garbage collector pacing docs.Google.com/document//… ↩︎

  24. Proposal: Separate soft and hard heap size goal go.Googlesource.com/proposal/+/… ↩︎

  25. src/runtime/mgc.go gowave.org/двуспальная кровать/runtime… ↩︎

  26. Go 1.5 concurrent garbage collector pacing docs.Google.com/document//… ↩︎

  27. runtime: separate soft and hard heap limits GitHub.com/gowaves/go/from… ↩︎