Как сборщик мусора предотвращает сканирование всей памяти при каждом сборе?

16

Некоторые (по крайней мере, Mono и .NET) сборщики мусора имеют область кратковременной памяти, которую они часто сканируют, и область вторичной памяти, которую они сканируют реже. Моно называет это детской.

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

Мой вопрос: как они предотвращают сканирование всей используемой памяти при каждом сборе? В принципе, единственный способ выяснить, какие объекты больше не используются, - это просмотреть все объекты и все их ссылки. Однако это предотвратит замену памяти операционной системой, даже если она не используется приложением и выглядит как огромный объем работы, которую необходимо выполнить, в том числе для «Коллекции питомников». Не похоже, что они много выигрывают, используя детскую.

Я что-то упускаю или сборщик мусора сканирует каждый объект и каждую ссылку каждый раз, когда выполняет сборку?

Питер ван Гинкель
источник
1
хороший обзор есть в статье «Настройка сбора мусора», написанной Анжеликой Лангер. Формально речь идет о том, как это делается в Java, но представленные концепции в значительной степени не зависят от языка
gnat

Ответы:

14

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

  1. После коллекции все существующие объекты будут иметь минимальное поколение (например, в .net, после коллекции Gen0 все объекты будут Gen1 или Gen2; после коллекции Gen1 или Gen2 все объекты будут Gen2).
  2. Объект или его часть, который не был написан со времен коллекции, которая продвинула все до поколения N или выше, не может содержать никаких ссылок на объекты более низких поколений.
  3. Если объект достиг определенного поколения, его не нужно идентифицировать как достижимый, чтобы обеспечить его сохранение при сборе нижестоящих поколений.

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

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

Supercat
источник
Перемещение блоков памяти обходится дорого в любой системе, поэтому улучшение развертки - это выигрыш даже для вашей четырехъядерной ГГц системы.
gbjbaanb
@gbjbaanb: Во многих случаях стоимость сканирования всего, чтобы найти живые объекты, была бы значительной и нежелательной, даже если бы перемещение объектов было абсолютно бесплатным. Следовательно, следует по возможности избегать сканирования старых объектов. С другой стороны, воздержание от сжатия старых объектов - это простая оптимизация, которая может быть выполнена даже на простых платформах. Кстати, если кто-то разрабатывает инфраструктуру GC для небольшой встроенной системы, декларативная поддержка неизменяемых объектов может оказаться полезной.
Сложно отследить,
... просто предположим, что изменяемые объекты необходимо сканировать при каждом проходе GC, а неизменяемые объекты - нет. Даже если единственный способ создать неизменный объект состоял в том, чтобы создать «прототип» в изменчивом пространстве и затем скопировать его, одна дополнительная операция копирования могла бы избежать необходимости сканировать объект в будущих операциях GC.
суперкат
Кстати, производительность сборки мусора в реализациях Microsoft BASIC 1980-х годов для микропроцессоров 6502 (и, возможно, других) также может быть значительно улучшена в некоторых случаях, если программа, генерирующая множество строк, которые никогда не изменятся, скопировала «следующее». Строковое распределение "указатель на указатель на" верхнюю часть строкового пространства ". Такое изменение помешало бы сборщику мусора проверить любую из старых строк, чтобы узнать, нужны ли они по-прежнему. Commodore 64 вряд ли был высокотехнологичным, но такой «поколенный» GC мог бы помочь даже там.
суперкат
7

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

«Но это неправильно», я слышу, как вы кричите, «старые объекты могут и действительно относятся к молодым объектам». Вы правы, и есть несколько решений для этого, которые все вращаются вокруг получения знаний, быстро и эффективно, какие старые объекты должны быть проверены, а какие безопасно игнорировать. Они в значительной степени сводятся к записи объектов или небольших (больше, чем объекты, но гораздо меньше, чем целая куча) областей памяти, которые содержат указатели на молодые поколения. Другие описывают это гораздо лучше меня, поэтому я просто дам вам пару ключевых слов: маркировка карточек, запоминающиеся наборы, барьеры для записи. Есть и другие методы (в том числе гибриды), но они охватывают общие подходы, которые я знаю.


источник
3

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

Райан Калпеппер
источник
1

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

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

ddyer
источник
Я не уверен, какие подходы к сбору мусора использовались в миникомпьютерах и мейнфреймах до конца 1970-х годов, но сборщик мусора Microsoft BASIC, по крайней мере на 6502 компьютерах, установит указатель «следующая строка» на верхнюю часть памяти, а затем выполнит поиск все строковые ссылки, чтобы найти самый высокий адрес, который был ниже «следующего строкового указателя». Эта строка будет скопирована чуть ниже «следующего указателя строки», а этот указатель будет припаркован чуть ниже нее. Затем алгоритм будет повторяться. Код мог сглаживать указатели, чтобы обеспечить ...
суперкат
... что-то вроде коллекции поколений. Иногда я задавался вопросом, насколько сложно было бы исправить BASIC для реализации «поколения» коллекции, просто сохраняя адреса вершины каждого поколения и добавляя несколько операций замены указателя до и после каждого цикла GC. Производительность GC все равно будет довольно плохой, но во многих случаях она может быть уменьшена с десятков секунд до десятых секунд.
суперкат
-2

В основном ... GC использует "сегменты", чтобы отделить то, что используется, а что нет. Как только он делает проверку, он стирает вещи, которые не используются, и перемещает все остальное во 2-е поколение (которое проверяется реже, чем 1-е поколение), а затем перемещает вещи, которые все еще используются, во 2-е ден в 3-е поколение.

Таким образом, вещи в 3-м поколении обычно являются объектами, которые по какой-то причине остаются открытыми, и GC проверяет их не очень часто.

aserwin
источник
1
Но как он узнает, какие объекты используются?
Питер ван Гинкель
Он отслеживает, какие объекты могут быть доступны из достижимого кода. Как только объект перестает быть доступным из любого кода, который может выполняться (скажем, кода для метода, который возвратился), тогда GC знает, что его безопасно собирать
JohnL
Вы оба, ребята, описываете, как правильные GC, а не как они эффективны. Судя по вопросу, ОП прекрасно это знает.
@delnan Да, я отвечал на вопрос, откуда он знает, какие объекты используются, что и было в комментарии Питера.
JohnL
-5

Алгоритм, обычно используемый этим GC, - Наивная метка-развертка.

Вам также следует учитывать тот факт, что этим управляет не сам C #, а так называемый CLR .

user827992
источник
Это чувство, которое я получил от чтения о сборщике мусора Моно. Тем не менее, я не понимаю, почему, если они сканируют весь рабочий набор на когда-либо собранный, у них есть сборщик поколений, с которым сбор GEN-0 очень быстрый. Как это может быть быстро с рабочим набором, скажем, 2 ГБ?
Питер ван Гинкель
ну, настоящий GC для mono - это Sgen, вы должны прочитать этот mono-project.com/Generational_GC или некоторые онлайн-статьи schani.wordpress.com/tag/mono infoq.com/news/2011/01/SGen , суть в том, что эти новые технологии, такие как CLR и CLI, имеют действительно модульную конструкцию, язык становится просто способом выражения чего-то для CLR, а не способом создания двоичного кода. Ваш вопрос касается деталей реализации, а не алгоритмов, потому что алгоритм все еще не имеет реализации, вы должны просто прочитать технические статьи и статьи из Mono, больше никого.
user827992
Я не совсем понимаю. Стратегия, которую использует сборщик мусора - это не алгоритм?
Питер ван Гинкель
2
-1 Перестань путать ОП. То, что GC является частью CLR и не зависит от языка, вообще не имеет значения. АЯ в основном характеризуются тем, как он вынимает кучу и определяет достижимость, а последнее все об алгоритме (ы) , используемом для этого. Хотя может быть много реализаций алгоритма, и вы не должны увлекаться деталями реализации, только алгоритм определяет, сколько объектов проверено. GC поколений - это просто схема алгоритма + кучи, которая пытается использовать «гипотезу поколений» (что большинство объектов умирает молодыми). Это не наивно.
4
Алгоритм! = Реализация действительно, но реализация может отклониться настолько далеко, прежде чем станет реализацией другого алгоритма. Описание алгоритма в мире GC очень специфично и включает в себя такие вещи, как не сканирование всей кучи в коллекции питомника и как найти и сохранить указатели между поколениями. Это правда, что алгоритм не говорит вам, сколько времени займет конкретный шаг алгоритма, но это не имеет никакого отношения к этому вопросу.