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 также использовал эту стратегию в первые дни Реализовал сборку мусора, но сегодняшние реализации намного сложнее.
Рисунок 7-21 Компоненты управления памятью
На приведенном выше рисунке пользовательская программа (Mutator) будет запрашивать память в куче через распределитель памяти (Allocator), а сборщик мусора (Collector) отвечает за освобождение памяти в куче. коллектор совместно управлять программой.куча пространства памяти в . В этом разделе мы подробно рассмотрим ключевые теории, связанные со сборкой мусора в Go, чтобы помочь нам лучше понять оставшуюся часть этого раздела.
пометить как очищенный
Алгоритм Mark-Sweep является наиболее распространенным алгоритмом сборки мусора.Сборщик Mark-Sweep является отслеживающим сборщиком мусора.Процесс его выполнения можно разделить на два этапа: Mark и Sweep:
- Фаза маркировки — начиная с корневого объекта, чтобы найти и отметить все оставшиеся объекты в куче;
- Фаза очистки — пройтись по всем объектам в куче, освободить непомеченные мусорные объекты и добавить освобожденную память в список свободных;
Как показано на рисунке ниже, пространство памяти содержит несколько объектов.Мы начинаем с корневого объекта и по очереди обходим подобъекты объекта и помечаем все объекты, достижимые из корневого узла, как живые, а именно три объекта A, C и D, а остальные три объекта B, E и F считаются мусором, поскольку они недоступны из корневого узла:
Рисунок 7-22 Фаза маркировки функции «Отметить очистку»
После того, как этап маркировки завершен, он переходит к фазе очистки.На этом этапе сборщик по очереди обходит все объекты в куче, освобождает три объекта B, E и F, которые не были отмечены, и подключает новые свободное место в памяти в структуре связанного списка.Вверх, удобно использовать распределитель памяти.
Рисунок 7-23. Фаза очистки для отметки «Отмена».
Здесь представлен наиболее традиционный алгоритм пометки и очистки.Сборщик мусора начинает работу с корневого объекта сборки мусора, рекурсивно обходит подобъекты, на которые указывают эти объекты, и помечает все достижимые объекты как живые, после окончания фазы маркировки сборщик мусора будет проходить объекты в куче по очереди и очищать мусор.Весь процесс должен отмечать состояние выживания объекта, и пользовательская программа не может выполняться во время процесса сборки мусора.Нам нужно использовать более сложный Механизм решения проблемы STW.
трехцветная абстракция
Чтобы устранить длительное время STW, вызванное исходным алгоритмом метки-очистки, большинство современных сборщиков мусора с отслеживанием реализуют вариант алгоритма трехцветной метки для сокращения времени STW. Алгоритм трехцветной маркировки делит объекты в программе на три категории: белые, черные и серые.4:
- белые объекты - потенциальный мусор, память которого может быть освобождена сборщиком мусора;
- Черные объекты — живые объекты, в том числе объекты, не имеющие ссылок на внешние указатели и объекты, достижимые из корневого объекта;
- Серые объекты — живые объекты, так как есть внешние указатели на белые объекты, сборщик мусора просматривает дочерние объекты этих объектов;
Рисунок 7-24 Трехцветный объект
Когда сборщик мусора начнет работать, в программе нет черных объектов, корневой объект сборки мусора будет помечен как серый, а сборщик мусора только достанет объекты из коллекции серых объектов и начнет сканирование, когда в коллекции серого объекта нет серого объекта, этап маркировки заканчивается.
Рисунок 7-25 Процесс выполнения сборщика мусора с трехцветной маркировкой
Принцип работы сборщика мусора с трехцветной маркировкой очень прост, и мы можем свести его к следующим шагам:
- Выберите серый объект из набора серых объектов и пометьте его черным;
- Отметьте все объекты, на которые указывает черный объект, как серые, чтобы гарантировать, что ни объект, ни объект, на который ссылается объект, не будут переработаны;
- Повторяйте два вышеуказанных шага до тех пор, пока в графе объектов не останется серых объектов;
Когда фаза маркировки очистки трехцветной метки завершена, в куче приложения нет серых объектов. Мы можем видеть только черные живые объекты и белые объекты мусора. Сборщик мусора может переработать этот белый мусор. Ниже приведена куча памяти после маркировки с помощью сборщика мусора с трехцветной маркировкой.Только объект D в куче является мусором, подлежащим сбору:
Рисунок 7-26 Куча после трехцветной маркировки
Поскольку пользовательская программа может изменить указатель объекта в процессе выполнения маркировки, сам алгоритм очистки трехцветной метки не может выполняться одновременно или инкрементно.Он по-прежнему требует STW.Во время процесса трехцветной маркировки, как показано ниже, пользовательская программа. С объекта A устанавливается ссылка на объект D, но поскольку серый объект больше не существует в программе, объект D будет неправильно собран сборщиком мусора.
Рисунок 7-27 Трехцветная метка и пользовательская программа
Объекты, которые не должны быть утилизированы, были утилизированы Это очень серьезная ошибка в управлении памятью Мы называем эту ошибку висячим указателем, то есть указатель не указывает на допустимый объект определенного типа, что влияет на безопасность Память.5, если вы хотите помечать объекты одновременно или постепенно, вам все равно нужно использовать барьерную технологию.
барьерная технология
Технология барьера памяти — это барьерная инструкция, которая позволяет ЦП или компилятору следовать определенным ограничениям при выполнении операций, связанных с памятью.Большинство современных процессоров в настоящее время выполняют инструкции не по порядку, чтобы максимизировать производительность, но эта технология может гарантировать код для последовательного характера памяти операции, операции, выполняемые до барьера памяти, должны предшествовать операциям, выполняемым после барьера памяти.6.
Чтобы обеспечить правильность параллельных или инкрементных алгоритмов маркировки, нам необходимо достичь любого из следующих двух трехцветных инвариантов:
- Сильная трихроматическая инвариантность - черные объекты не указывают на белые объекты, а только на серые или черные объекты;
- Слабая трихроматическая инвариантность - белый объект, на который указывает черный объект, должен содержать достижимый путь от серого объекта через несколько белых объектов.7;
Рисунок 7-28 Трехцветная инвариантность
На приведенном выше рисунке показана память кучи, которая соответствует строгой трехцветной инвариантности и слабой трехцветной инвариантности соответственно Следуя любому из двух вышеуказанных инвариантов, мы можем гарантировать правильность алгоритма сборки мусора, а барьерная технология находится в параллелизме или Важная техника для обеспечения трехцветная инвариантность при инкрементной маркировке.
Техника барьера в сборке мусора больше похожа на метод ловушки.Это фрагмент кода, который выполняется, когда пользовательская программа читает объект, создает новый объект и обновляет указатель объекта.В зависимости от типа операции мы можем разделить их на барьеры чтения (Read Barrier) и барьер записи (Write Barrier), поскольку барьеру чтения необходимо добавлять фрагменты кода в операции чтения, что оказывает большое влияние на производительность пользовательской программы, поэтому языки программирования часто используйте барьеры записи для обеспечения трехцветной инвариантности.
Здесь мы хотим представить две технологии барьера записи, используемые в языке Go, а именно барьер записи вставки, предложенный Дейкстрой.8и предложение Юасы убрать барьер записи9, здесь мы разберем, как они гарантируют трехцветную инвариантность и корректность сборщика мусора.
Вставить барьер записи
Дейкстра предложил вставить барьеры записи в 1978 году. Через барьеры записи, как показано ниже, пользовательская программа и сборщик мусора могут работать попеременно, чтобы обеспечить правильность выполнения программы:
writePointer(slot, ptr):
shade(ptr)
*field = ptr
Вышеуказанный псевдокод для вставки барьера записи довольно легко понять, когда мы делаем что-то вроде*slot = ptr
выражение, мы выполним указанный выше барьер записи черезshade
Функция пытается изменить цвет указателя. еслиptr
Если указатель белый, функция делает объект серым, в противном случае он остается прежним.
Рисунок 7-29 Дейкстра вставляет барьер записи
Предполагая, что мы используем барьер записи вставки, предложенный Дейкстрой в нашем приложении, в сценарии, где сборщик мусора и пользовательская программа запускаются попеременно, будет происходить процесс маркировки, показанный на рисунке выше:
- Сборщик мусора помечает корневой объект, указывающий на объект A, черным, а объект B, на который указывает объект A, — серым;
- Программа пользователя изменяет указатель объекта A и направляет указатель, первоначально указывающий на объект B, на объект C. В это время срабатывает барьер записи, чтобы пометить объект C как серый;
- Сборщик мусора по очереди проходит другие серые объекты в программе, помечая их как черные;
Вставка барьера записи Дейкстры представляет собой относительно консервативную барьерную технологию, котораяОбъекты, которые могут выжить, отмечены серым цветом.Для того, чтобы соответствовать строгой трихроматической инвариантности. Во время процесса сборки мусора, показанного выше, объекты 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
Приведенный выше код окрасит белый старый объект в серый цвет, когда ссылка на старый объект будет удалена, так что удаление барьера записи может обеспечить слабую трехцветную инвариантность, а нижестоящий объект, на который ссылается старый объект, должен ссылаться на серый объект. .
Рисунок 7-29 Yuasa удаляет барьер записи
Предполагая, что мы используем барьер удаления записи, предложенный Юасой в приложении, в сценарии, когда сборщик мусора и пользовательская программа запускаются попеременно, появится процесс маркировки, показанный на рисунке выше:
- Сборщик мусора помечает корневой объект, указывающий на объект A, черным, а объект B, на который указывает объект A, — серым;
- Пользовательская программа для исходного указателя объекта на точку B, C, вызывающая удаление барьера записи, но поскольку объект B уже серый, ничего не нужно менять;
- Пользовательская программа удаляет указатель, который объект B изначально указывал на C, вызывая удаление барьера записи, и белый объект C окрашивается в серый цвет.;
- Сборщик мусора по очереди проходит другие серые объекты в программе, помечая их как черные;
Третий шаг в описанном выше процессе запускает затенение Yuasa для удаления барьера записи, поскольку пользовательская программа удаляет указатель B на объект C, поэтому два объекта C и D нарушат строгую трехцветную инвариантность и слабую трехцветную инвариантность соответственно. :
- Строгая трихроматическая инвариантность - черный объект A указывает прямо на белый объект C;
- Слабая трехцветная инвариантность — сборщик мусора не может стартовать с серого объекта и обращаться к белым объектам C и D через несколько последовательных белых объектов;
Yuasa устраняет барьер записи, окрашивая объект C, чтобы гарантировать, что объект C и нижестоящий объект D могут пережить этот цикл сборки мусора, избегая оборванных указателей, чтобы гарантировать правильность пользовательской программы.
Инкрементный и параллельный
Традиционный алгоритм сборки мусора приостанавливает приложение во время выполнения сборки мусора. После запуска сборки мусора сборщик мусора вытесняет использование ЦП и занимает большое количество вычислительных ресурсов для завершения работы по маркировке и очистке.Однако , многие приложения, использующие длинные STW в реальном времени, неприемлемы.
Рисунок 7-30 Сборка мусора и программа паузы
Вычислительных ресурсов в древние времена не было так много, как сегодня. Современные компьютеры часто представляют собой многоядерные процессоры. Как только сборщик мусора начнет выполняться, он будет тратить много вычислительных ресурсов. Время паузы, мы оптимизируем современные сборщики мусора, используя следующую стратегию. :
- Инкрементная сборка мусора — постепенно помечает и удаляет мусор, сокращая максимальное время приостановки приложения;
- Параллельная сборка мусора. Используйте преимущества многоядерных вычислительных ресурсов для одновременной пометки и удаления мусора во время выполнения пользовательских программ.
Поскольку как инкрементальный, так и параллельный режимы могут работать попеременно с пользовательскими программами, нам нужноИспользуйте барьерную технологиюОбеспечить правильность сборки мусора, при этом приложение не может ждать переполнения памяти для запуска сборки мусора, потому что при нехватке памяти приложение не смогло выделить память, что ничем не отличается от прямой приостановки работы программы, добавочная и параллельная сборка мусора должна запускаться заранее и завершать весь цикл до исчерпания памяти, избегая длительных пауз в программе.
Инкрементный коллектор
Инкрементная сборка мусора — это решение, позволяющее сократить самое длинное время паузы программы.Он может разделить первоначально долгое время паузы на несколько меньших временных срезов GC, хотя время от начала до конца сборки мусора больше, но это также уменьшает максимальное время приостановки приложения:
Рисунок 7-31 Инкрементный сборщик мусора
Следует отметить, что инкрементальную сборку мусора необходимо использовать вместе с методом трехцветной маркировки.Для обеспечения корректности сборки мусора нам необходимо открыть барьер записи до начала сборки мусора, чтобы модификация памяти пользовательская программа будет написана первой.Обработка барьера обеспечивает сильную трехцветную инвариантность или слабую трехцветную инвариантность отношения объектов в куче памяти. Хотя добавочная сборка мусора может сократить максимальное время паузы программы, добавочная сборка также увеличивает общее время цикла GC. Во время сборки мусора пользовательские программы также должны нести дополнительные вычислительные затраты из-за влияния барьеров записи. Таким образом, добавочная сборка мусора не единственное преимущество.
параллельный коллектор
Параллельная сборка мусора может не только сократить самое продолжительное время паузы программы, но и сократить время всей фазы сборки мусора.Воспользуйтесь преимуществами нескольких ядер для параллельного выполнения пользовательских программ., параллельный сборщик мусора снижает влияние сборки мусора на приложение:
Рисунок 7-31 Параллельный сборщик мусора
Хотя параллельный сборщик может работать с пользовательской программой, не все этапы могут выполняться с пользовательской программой, и некоторые этапы по-прежнему требуют приостановки пользовательской программы, но по сравнению с традиционным алгоритмом параллельная сборка мусора может выполняться одновременно. выполняться максимально параллельно, конечно, из-за введения барьеров чтения-записи параллельные сборщики мусора обязательно принесут дополнительные накладные расходы, которые не только увеличат общее время сборки мусора, но и повлияют на пользовательские программы. проектируют сборку мусора, необходимо учитывать стратегию.
7.2.2 Процесс эволюции
Сборщик мусора языка Go развивается с первого дня своего рождения, за исключением нескольких версий без крупных обновлений, почти каждая выпущенная минорная версия будет улучшать производительность сборки мусора, а производительность будет улучшаться вместе с ней. Учитывая сложность кода сборщика мусора, в этом разделе будет проанализирована эволюция сборщика мусора по сравнению с версией языка Go v1.0.
- v1.0— полностью последовательная пометка и четкий процесс, требующий приостановки всей программы;
- v1.1— Параллельно выполнять этапы маркировки и очистки сборки мусора на многоядерных хостах.11;
-
v1.3- время выполнения на основеТолько значения типа pointer содержат указателиПредположение добавляет поддержку точного сканирования памяти стека, обеспечивая действительно точную сборку мусора.12;
- Буду
unsafe.Pointer
Значение, преобразованное в целочисленный тип, считается недопустимым, что может вызвать серьезные проблемы, такие как висячие указатели;
- Буду
-
v1.5- Реализовано на основеПараллельность разверток трехцветных маркеровуборщик мусора13;
- Значительно сократить задержку сборки мусора с нескольких сотен мс до менее 10 мс;
- Рассчитать подходящее время для запуска сборки мусора и ускорить процесс сборки мусора за счет параллелизма;
-
v1.6- Достигнутодецентрализациякоординатор по вывозу мусора;
- На основе явного конечного автомата любая горутина может инициировать переход состояния сборки мусора;
- Интенсивное использование альтернативного растрового представления списка свободной кучи, что снижает стадию очистки загрузки ЦП.14;
- v1.7- пройти черезПараллельное сжатие стекаСокращение времени сборки мусора до менее 2 мс15;
- v1.8- использоватьсмешанный барьер записиСокращение времени сборки мусора до менее 0,5 мс16;
- v1.9— полностью убрать процесс пересканирования стека остановленной программы17;
- v1.10— Обновлена реализация Garbage Collection Pacer для разделения целей жесткого и мягкого размера кучи.18;
- v1.12- использоватьАлгоритм завершения новой меткиУпростить несколько этапов сборщика мусора19;
- v1.13— Решить проблему возврата памяти в операционную систему для приложений с временным высоким использованием памяти с помощью нового Scavenger20;
- v1.14— использовать новый распределитель страницОптимизировать скорость выделения памяти21;
Из эволюции сборщика мусора языка Go видно, что реализация и алгоритм этого компонента становятся все более и более сложными.Первоначальный сборщик мусора все еще был неточным однопоточным сборщиком STW, но последняя версия сборщика мусора Поддерживаются такие функции, как одновременная сборка мусора, децентрализованная координация и т. д., и здесь мы представим компоненты и функции, связанные с последней версией сборщика мусора.
одновременная сборка мусора
Язык Go представил параллельный сборщик мусора в версии 1.5.Сборщик мусора использует трехцветную абстракцию и технологию барьера записи, о которой мы упоминали выше, чтобы гарантировать правильность выполнения сборщика мусора.Как реализовать параллельный сборщик мусора в подробности здесь, давайте взглянем на рабочий процесс некоторых параллельных сборщиков мусора.
Во-первых, параллельный сборщик мусора должен запускать цикл сборки мусора в нужный момент.Если предположить, что наша программа на языке Go работает на 4-ядерной физической машине, то после запуска сборки мусора сборщик будет занимать 25% вычислительных ресурсов. в фоновом режиме для сканирования и маркировки объектов в памяти:
Рисунок 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-кратного размера предыдущей сборки мусора. поведение может быть достигнуто с помощью переменных окруженияGOGC
Adjustment, по умолчанию его значение равно 100, то есть 100% увеличение памяти кучи вызовет GC.
Рисунок 7-33 Время сбора мусора сборщиком мусора STW
Поскольку параллельный сборщик мусора будет работать вместе с программой, он не может точно контролировать размер кучи памяти. Параллельный сборщик мусора должен инициировать сборку мусора до достижения цели, чтобы обеспечить контролируемый размер памяти. Гарантированная память кучи в конце сборка мусора такая же, как настроенная пользователемGOGC
Последовательный.
Рисунок 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:
Рисунок 7-35 Несколько этапов сборки мусора
- фаза завершения очистки;
- Приостановить программу, в это время все процессоры войдут в безопасную точку;
- Если текущий цикл сборки мусора срабатывает принудительно, нам также необходимо иметь дело с не очищенными блоками управления памятью;
- Отметить фазу;
- переключить состояние на
_GCmark
, включите барьер записи, поддержку программы пользователя (Mutator Assiste) и поставьте корневой объект в очередь; - Возобновление выполнения, маркировка процессов и вспомогательные пользовательские программы начнут одновременно маркировать объекты в памяти, барьеры записи будут помечать как перезаписанные указатели, так и новые указатели серым цветом, а все вновь созданные объекты будут непосредственно помечаться черным цветом;
- Начать сканирование корневых объектов, включая все стеки Goroutine, глобальные объекты и структуры данных времени выполнения, которые не находятся в куче. Текущий процессор будет приостановлен во время сканирования стека Goroutine;
- Обрабатывайте объекты в серой очереди по очереди, помечая объекты черным цветом, а объекты, на которые они указывают, серым цветом;
- Используйте алгоритм распределенного завершения для проверки оставшейся работы и перейдите к этапу завершения тега после завершения этапа маркировки;
- переключить состояние на
- отметить стадию завершения;
-
Приостановить программу, переключите состояние на
_GCmarktermination
и закрыть программу пользователя вспомогательного маркера; - очистить кеш потоков на процессоре;
-
Приостановить программу, переключите состояние на
- этап очистки;
- переключить состояние на
_GCoff
Запустите фазу очистки, инициализируйте состояние очистки и закройте барьер записи; - Восстановите программу пользователя, все вновь созданные объекты будут отмечены белым цветом;
- Все единицы управления памятью очищаются одновременно в фоновом режиме, и очистка запускается, когда 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
}
-
gcTriggerHeap
— Выделение кучи памяти достигает триггерного размера кучи, рассчитанного контроллером; -
gcTriggerTime
— При отсутствии триггера в течение определенного периода времени будет запущен новый цикл, а условие запуска определяетсяruntime.forcegcperiod
Переменное управление, по умолчанию 2 минуты; -
gcTriggerCycle
- Если текущая сборка мусора не включена, запускается новый цикл;
Метод, используемый для включения сборки мусораruntime.gcStart
получитruntime.gcTrigger
тип предиката, мы можем запустить на основе этого_GCoff
Выходящая структура находит код для всех запущенных сборок мусора:
-
runtime.sysmon
а такжеruntime.forcegchelper
— запуск периодических проверок и сбор мусора в фоновом режиме; -
runtime.GC
- Пользовательская программа запускает сборку мусора вручную; -
runtime.mallocgc
- запускать размер кучи сборки мусора в соответствии со временем памяти приложения;
Рисунок 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)
}
- Прежде чем официально начать сборку мусора, среда выполнения должна пройти
runtime.gcWaitOnMark
Функция ожидает завершения фаз маркировки-завершения, маркировки-и-маркировки-завершения предыдущего цикла; - передача
runtime.gcStart
Запустить новый раунд сбора мусора и пройтиruntime.gcWaitOnMark
Подождите, пока отмеченная фаза завершения этого раунда сборки мусора завершится нормально; - Продолжай звонить
runtime.sweepone
Очищает все ожидающие единицы управления памятью и ожидает завершения всей работы по очистке, во время которой она будет вызванаruntime.Gosched
отказаться от процессора; - Завершив очистку этого раунда сборки мусора, передайте
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
}
- При отсутствии свободного места в блоке управления памятью текущего потока создание микрообъектов и мелких объектов требует вызова
runtime.mcache.nextFree
Метод получает новую единицу управления из центрального кэша или кучи страниц, после чего может быть запущена сборка мусора; - Когда пользовательская программа запрашивает выделение большого объекта размером более 32 КБ, он будет создан
runtime.gcTrigger
Структура пытается инициировать сборку мусора;
Запуск сборки мусора через память кучи требует сравненияruntime.mstats
Два поля в — представляют количество байтов живых объектов в сборке мусора.heap_live
и размер кучи памяти, представляющий метку триггераgc_trigger
; Когда количество байтов живых объектов в памяти превышает размер кучи, запускающий сборку мусора, запускается новый раунд сборки мусора. Здесь мы представим процесс расчета этих двух значений отдельно:
-
heap_live
— Для уменьшения конкуренции за блокировки среда выполнения будет обновляться только тогда, когда центральный кеш выделяет или освобождает блоки управления памятью и выделяет большие объекты в куче; -
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
И проведите некоторую подготовительную работу, мы будем внедрять реализацию этой функции в следующие этапы:
- позвони дважды
runtime.gcTrigger.test
Метод проверяет, выполняются ли условия сборки мусора; - Приостановить программу, запустить работник Goroutine на заднем плане, чтобы обрабатывать задачу маркировки, убедитесь, что все блоки управления памятью очищены, а другие препараты до начала маркировки начнется;
- Войдите в фазу маркировки, подготовьтесь к работе в фоновом режиме, отметьте корневой объект и микрообъекты, восстановите программу пользователя и войдите в фазу одновременного сканирования и маркировки;
Этот метод также вызывается постоянно в цикле при проверке условия сборки мусора.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
и последовательно выполните следующие действия:
- передача
runtime.gcBgMarkPrepare
Функция инициализирует состояние, требуемое фоновым сканированием; - передача
runtime.gcMarkRootPrepare
Функция сканирует стек, глобальные переменные и другие корневые объекты и добавляет их в очередь; - установить глобальную переменную
runtime.gcBlackenEnabled
, пользовательские программы и задачи маркировки могут затемнять объекты; - передача
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
, реализация этой функции относительно проста:
- передача
runtime.netpoll
Получить ожидающие задачи из сетевого опросника и присоединиться к глобальной очереди; - передача
runtime.procresize
Расширить или уменьшить глобальный процессор; - передача
runtime.notewakeup
илиruntime.newm
Разбудите процессор по очереди или создайте новый поток для процессора; - Если количество ожидающих выполнения горутин слишком велико, создайте дополнительные процессоры, чтобы помочь в выполнении задачи;
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
Функция выполняет маркировку объекта этой горутины одновременно на текущем процессоре:
Рисунок 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
Сопрограммы режима гарантируют загрузку ЦП.
Рис. 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
Это функция, выполняемая задачей маркировки в фоновом режиме. Сканирование и маркировка графа объектов в памяти выполняются в цикле этой функции. Мы представляем принцип реализации этой функции в трех частях:
- Получите текущий процессор и горутину, упакованную в
parkInfo
тип строения и активно засыпают в ожидании пробуждения; - По процессору
gcMarkWorkerMode
Режим определяет стратегию сканирования задач; - После того, как все задачи завершены отметку, позвоните
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
, эта структура является абстракцией рабочего пула в сборщике мусора. Она реализует модель производителя и потребителя. Мы можем использовать эту структуру в качестве отправной точки для понимания отмеченной работы в целом:
Рисунок 7-39 Рабочий пул сборщика мусора
Барьеры записи, сканирование корневых объектов и сканирование стека добавят дополнительные серые объекты в рабочий пул для обработки, а процесс сканирования объектов пометит серые объекты как черные, а также могут быть найдены новые серые объекты. не содержит Когда объект становится серым, весь процесс сканирования завершается.
Чтобы уменьшить конфликт блокировок, среда выполнения будет сохранять независимую работу для сканирования на каждом процессоре, но это столкнется с той же проблемой, что и планировщик — ресурсы разных процессоров распределены неравномерно, в результате чего некоторым процессорам нечего делать, Планировщик вводит кражу работы для решения этой проблемы, а сборщик мусора использует аналогичный механизм для балансировки отложенных задач на разных процессорах.
Рисунок 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
функция, когда загрузка ЦП превышаетfractionalUtilizationGoal
20% от возврата; -
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
Они отвечают за "заимствование" и "погашение долгов" соответственно. С помощью этой системы управления долгом мы можем гарантировать, что горутины работают нормально, не вызывая слишком большого давления на сборку мусора, и гарантировать, что фаза маркировки будет завершена, когда целевой размер кучи будет достигнут. достиг.
Рисунок 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)
}
}
Если вспомогательная очередь не пуста, вышеуказанная функция решит, следует ли разбудить эти бездействующие горутины в соответствии с суммой долга и выполненной работой каждой горутины; если все горутины пробуждены и все еще остаются отмеченные задачи, эти отмеченные задачи будут добавлены Глобальный кредит.
Рисунок 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 Дополнительная литература
- Garbage Collection In Go : Part I - Semantics
- Getting to Go: The Journey of Go's Garbage Collector
- The Garbage Collection Handbook
- Immix: A Mark-Region Garbage Collector with Space Efficiency, Fast Collection, and Mutator Performance
- Перейти к сборке мусора с низкой задержкой
- GopherCon 2015: Rick Hudson - Go GC: Solving the Latency Problem
- Go GC: Latency Problem Solved
- Concurrent garbage collection
- Design and Implementation of a Comprehensive Real-time Java Virtual Machine
-
«Сборщики мусора — последовательный, параллельный, CMS, G1 (и что нового в Java 8)»blog.over о PS.com/garbage-col… ↩︎
-
«Обзор управления памятью в Rust»ПК Уолтон.GitHub.IO/2013/03/18/… ↩︎
-
«Автоматический подсчет ссылок (ARC) Objective-C»Surf.lilumu.org/docs/auto… ↩︎
-
«Трехцветная маркировка»Итак, Wikipedia.org/wiki/Трейси, ты… ↩︎
-
"Висячий указатель"En. Wikipedia.org/wiki/dang… ↩︎
-
«Википедия: Барьер памяти»En. Wikipedia.org/wiki/memory… ↩︎
-
П. П. Пиринен, Барьерные методы для инкрементной трассировки, Извещения ACM SIGPLAN, 34(3), 20–25, октябрь 1998 г.Войдите на .ACM.org/do i/10.1145… ↩︎
-
Дейкстра Э. В., Лэмпорт Л., Мартин А. Дж., Шолтен К. С., Стеффенс Э. Ф. Оперативная сборка мусора: упражнение в сотрудничестве, Коммюнике ACM, 21(11), 966–975, 1978.Woohoo. В это время. u Texas. quota/users/E WD/he… ↩︎
-
Юаса Т. Сборка мусора в реальном времени на машинах общего назначения Journal of Systems and Software, 11(3):181–198, 1990.woohoo.science direct.com/science/art… ↩︎
-
Пол Р. Уилсон, «Методы сборки мусора для однопроцессорных систем».Woohoo. В это время. Растения. Квота/~Счет/курсы… ↩︎
-
Performance · Go 1.1 Release Notes Go Waves .org / doc / go1.1 # боится ... ↩︎
-
Changes to the garbage collector · Go 1.3 Release Notes golang.org/doc/go1.3#… ↩︎
-
Go 1.5 concurrent garbage collector pacing docs.Google.com/document//… ↩︎
-
Остин Клементс, 2015. «Предложение: плотные биты меток и распределение без развертки»go.google source.com/proposal/+/... ↩︎
-
runtime: shrinkstack during mark termination significantly increases GC STW time GitHub.com/golang/go/i… ↩︎
-
Proposal: Eliminate STW stack re-scanning go.Googlesource.com/proposal/+/… ↩︎
-
Proposal: Eliminate STW stack re-scanning go.Googlesource.com/proposal/+/… ↩︎
-
Proposal: Separate soft and hard heap size goal go.Googlesource.com/proposal/+/… ↩︎
-
Proposal: Simplify mark termination and eliminate mark 2 ttps://go.googlesource.com/proposal/+/master/design/26903-simplify-mark-termination.md ↩︎
-
Proposal: Smarter Scavenging go.Googlesource.com/proposal/+/… ↩︎
-
Proposal: Scaling the Go page allocator go.Googlesource.com/proposal/+/… ↩︎
-
Остин Клементс, 2015 г. «Предложение: децентрализованная координация GC».go.Googlesource.com/proposal/+/… ↩︎
-
Go 1.5 concurrent garbage collector pacing docs.Google.com/document//… ↩︎
-
Proposal: Separate soft and hard heap size goal go.Googlesource.com/proposal/+/… ↩︎
-
src/runtime/mgc.go gowave.org/двуспальная кровать/runtime… ↩︎
-
Go 1.5 concurrent garbage collector pacing docs.Google.com/document//… ↩︎
-
runtime: separate soft and hard heap limits GitHub.com/gowaves/go/from… ↩︎