Letysite.ru

IT Новости с интернет пространства
0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Сборка мусора c

Сборка мусора и деструкторы

C# — Руководство по C# — Сборка мусора и деструкторы

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

Именно по этой причине одной из главных функций любой схемы динамического распределения памяти является освобождение памяти от неиспользуемых объектов, чтобы сделать ее доступной для последующего перераспределения. Во многих языках программирования освобождение распределенной ранее памяти осуществляется вручную. Например, в С++ для этой цели служит оператор delete. Но в C# применяется другой, более надежный подход: «сборка мусора».

Система «сборки мусора» в C# освобождает память от лишних объектов автоматически, действуя незаметно и без всякого вмешательства со стороны программиста. «Сборка мусора» происходит следующим образом. Если ссылки на объект отсутствуют, то такой объект считается ненужным, и занимаемая им память в итоге освобождается и накапливается. Эта утилизированная память может быть затем распределена для других объектов.

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

Деструкторы

В языке C# имеется возможность определить метод, который будет вызываться непосредственно перед окончательным уничтожением объекта системой «сборки мусора». Такой метод называется деструктором и может использоваться в ряде особых случаев, чтобы гарантировать четкое окончание срока действия объекта. Например, деструктор может быть использован для гарантированного освобождения системного ресурса, задействованного освобождаемым объектом. Ниже приведена общая форма деструктора:

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

Давайте рассмотрим пример использования деструкторов:

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

Реализация сборки мусора на С++


Автор: Михаил Чащин
Источник: RSDN Magazine #1

Опубликовано: 18.11.2002
Исправлено: 13.03.2005
Версия текста: 1.0

В данной статье мы рассмотрим обобщённую реализацию сборки мусора на С++. Будут обсуждены два конкретных алгоритма сборки мусора – “Mark-Sweep” и “Mark-Compact”, и их реализация. Мы также рассмотрим ограничения, которые накладываются на приложения при использовании сборки мусора, и изменения в компиляторе C++, которые могли бы помочь избежать этих ограничений.

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

Использование сборки мусора, как одного из вариантов автоматического управления памятью, многими разработчиками в наши дни рассматривается как необходимый шаг к обеспечению надёжности программы, поскольку в этом случае код для работы с памятью собран в одном чётко определённом модуле, а не разбросан в разных частях приложения. В этом случае с одной стороны мы получаем целый ряд преимуществ, в числе которых решение проблемы повисших указателей (dangling pointers) и утечек памяти (memory leaks), но, с другой стороны, мы теряем гибкость, поскольку сборка мусора накладывает некоторые ограничения на разработку приложения.

Если сборка мусора не встроена в язык программирования, её очень тяжело использовать. Кроме этого, возможны потери как в скорости, так и в памяти. Поэтому общепризнанным является тот факт, что самого эффективного автоматического управления памятью в приложении можно добиться только в том случае, если это управление поддерживается самим языком программирования. К сожалению, сегодня стандартный C++ эту возможность не поддерживает. Поэтому программистам, желающим использовать сборку мусора, приходится реализовывать тот или иной алгоритм сборки мусора вручную. Самым распространённым вариантом является так называемый подсчёт ссылок. Правда, этот алгоритм содержит крупный недостаток – проблему утилизации цикличных графов.

Надо отметить, что автоматическая сборка мусора не включена в стандарт C++ по той простой причине, что программа, её использующая, будет всегда работать медленнее, чем если бы сборка мусора не использовалась вообще. Поэтому Бьёрном Страуструпом было предложено перепоручить обязанности сборки мусора внешним библиотекам, не затрагивая самого C++, что может позитивно сказаться на производительности приложений, поскольку программист сам может решить, где и когда ему стоит использовать автоматическое управление памятью. Это и является серьёзным отличием С++ от Java – при использовании Java у программистов просто нет выбора.

Сборка мусора логически состоит из двух частей:

  1. Поиск мусора – процесс поиска недостуных для приложения объектов;
  2. Удаление мусора – процесс утилизации памяти для дальнейшего её использования.

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

Таким образом, сборка мусора использует критерий доступности объекта при своей работе. Доступные объекты, в свою очередь, условно можно разделить на два вида: объекты, непосредственно доступные программе, и объекты, доступные косвенно, то есть доступные только через указатели внутри других объектов. Объекты первого вида часто называют периметром, а второго – объектами внутри периметра. Поэтому мы будем говорить, что всё, что находится за пределами периметра, недоступно программе и, следовательно, подлежит удалению.

В дальнейшем мы будем называть объекты, с которыми работает сборка мусора, управляемыми (managed) объектах, в отличие от неуправляемых (unmanaged) объектов, которые не подвержены процессу сборки мусора.

Анализ

Для начала разберемся, как найти периметр. Если объект на периметре непосредственно доступен в приложении, то у нас есть по крайней мере один указатель на этот объект. Если же таких указателей нет, объект не находится на периметре.

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

Следующий вопрос заключается в поиске объектов внутри периметра, то есть таких объектов, которые доступны косвенно через объекты на периметре. Запрашивая каждый объект на периметре, чтобы он перечислил все свои внутренние указатели на объекты, мы получим часть объектов внутри периметра (первый уровень). Если, в свою очередь, их также опросить обо всех объектах, на которые они имеют ссылки, мы получим ещё один набор объектов внутри периметра. Продолжая так до тех пор, пока существуют косвенные ссылки, мы найдём все объекты внутри периметра. Легко заметить, что объекты внутри периметра образуют граф, который может содержать зацикливающиеся ссылки. Поэтому, во избежание зацикливания, необходимо каким-то образом отличать уже опрошенные объекты от не опрошенных. Это можно сделать либо создав список опрошенных объектов, либо добавлением в объекты специального флага. Мы воспользуемся вторым способом. Установка этого флага будет говорить о том, что объект находится внутри периметра, и что он уже был опрошен о внутренних ссылках. Если мы установим этот флаг и у всех объектов на периметре, то поиск мусора, или объектов вне периметра, сведётся к простой проверке объектов на сброшенность флага. Последнее утверждение подразумевает, что у нас есть возможность перебирать все объекты.

Перебор объектов – задача несложная, если упорядочить их создание. Но, если мы также планируем производить дефрагментацию памяти, то есть планируется перемещать объекты, то одного организованного создания объектов будет недостаточно. Придётся также обновлять все указатели на переместившиеся объекты, а это означает, что и указатели должны создаваться упорядоченно. Чтобы упростить архитектуру сборки мусора, мы воспользуемся следующим приёмом: для каждого объекта будем использовать всего лишь один указатель, который будет обновляться каждый раз, когда объект перемещается. В то же время все указатели в приложении будут ссылаться не непосредственно на объект, а на его указатель. Это даёт нам возможность, с одной стороны, безболезненно производить дефрагментацию памяти, поскольку для каждого объекта существует только один указатель, который надо обновить, а с другой стороны, мы сможем инкапсулировать процесс подсчёта ссылок и создания объектов, если использовать идиому smart-указателей.

Итак, мы имеем следующее решение: все объекты создаются упорядоченно, чтобы их можно было перемещать, уменьшая тем самым фрагментацию памяти. Для каждого объекта существует ровно один smart-указатель, который инкапсулирует создание объекта, подсчёт ссылок, а также включает флаг, необходимый для определения принадлежности объекта к периметру. Все указатели на smart-указатели могут создаваться как угодно, но как только они создаются, они должны увеличивать счётчик ссылок smart-указателя, а при уничтожении соответственно его уменьшать. Поэтому такие указатели лучше всего также сделать smart-указателями (в дальнейшем мы будем называть их дескрипторами).

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

Дизайн

Посмотрим на обобщённый дизайн (generic design) будущей системы. Начнём со smart-указателей. Как уже упоминалось выше, в их обязанность входит подсчёт ссылок, который может быть реализован по-разному, и в нашем случае не влияет на остальную функциональность класса smart-указателей. Поэтому представляется хорошей идеей вынести подсчёт ссылок в отдельный класс. Использование функциональности такого класса в классе smart-указателей возможно несколькими способами. Мы воспользуемся наследованием. Класс подсчёта ссылок должен реализовывать следующие функции:

  • void AddRef() – вызывается при добавлении ссылки на объект;
  • void Release() – вызывается при удалении ссылки на объект;
  • bool active() const – используется сборщиком мусора для определения того, что объект принадлежит периметру, то есть его счётчик не равен нулю.

Примером класса с таким интерфейсом может быть следующий шаблон:

Здесь параметр T шаблона – это тип, который выбирается программистом для переменной, хранящей количество ссылок. Например, если на один объект может существовать огромное количество указателей, то в шаблон лучше всего передать тип unsigned long. Если же нет, то может подойти и unsigned char.

Чтобы иметь возможность перечислять объекты периметра, необходимо уметь перечислять smart-указатели, у которых счётчик ссылок отличен от нуля. Поэтому нужно контролировать процесс создания smart-указателей. Эту функциональность мы также можем вынести в отдельный класс (назовём его SPAllocator), от которого в дальнейшем унаследуем класс smart-указателей. Ниже приведён список функций, которые образуют интерфейс для создания и перечисления smart-указателей:

  • static void* operator new(size_t) – создание smart-указателя;
  • static void operator delete(void*) – удаление smart-указателя;
  • static SPIterator iterator() – возвращает итератор списка smart-указателей.

Класс SPIterator – это вложенный в SPAllocator открытый класс. Прототип шаблона класса SPAllocator приведён в листинге 2.

При создании smart-указателя происходит непосредственное создание объекта. Удаление smart-указателя влечёт за собой удаление объекта. Кроме процедур создания и удаления может понадобиться процедура уплотнения памяти. Поэтому класс для управления жизнью объектов (назовём его ObjectAllocator) должен предоставлять следующие три функции:

  • static void* Allocate(size_t) – возвращает адрес памяти, выделенной для объекта;
  • static void Deallocate(void*) – удаляет объект по заданному адресу памяти;
  • static void Compact() – вызывается для дефрагментации памяти.

Функция Allocate может автоматически вызывать сборку мусора при нехватке памяти, чтобы освободить хотя бы часть ресурсов. В то же время функция Compact должна передвигать объекты в памяти, и, следовательно, должна знать о smart-указателях. Поэтому класс ObjectAllocator – это шаблон, в качестве параметров которого выступают класс сборки мусора и класс smart-указателей (см. ниже). Прототип класса приведён в листинге 3.

Итак, после разделения функциональности класса smart-указателя на непересекающиеся части, мы получаем обобщённый шаблон класса smart-указателя:

где T – тип создаваемого объекта, R – класс, инкапсулирующий подсчёт ссылок (например, UnsafeRefCounter из листинга 1), S – класс-шаблон, отвечающий за создание и перечисление smart-указателей (прототип такого класса – SPAllocator – приведён в листинге 2), GC – класс сборки мусора (см. ниже), W – класс, инкапсулирующий создание объекта (прототип такого класса – ObjectAllocator – приведён в листинге 3).

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

Сборка мусора и финализаторы

В статье «Типы данных» дана характеристика и назначение двух сегментов оперативной памяти: стека (stack) и управляемой кучи (heap), отмечена эффективность стека при управлении памятью (в его работу вы при всем желании вмешаться не сможете). Работа с кучей, в которой размещаются объекты, предполагает использование автоматических сборщиков мусора (Garbage collector). Сбор мусора — это симуляция бесконечной памяти на машине с конечной памятью.
При создании приложений на C# можно полагать, что исполняющая среда .NET будет сама заботиться об управляемой куче без непосредственного вмешательства со стороны программиста. Вам понравится «золотое правило» по управлению памятью в .NET:
Размещайте объект в управляемой куче с использованием ключевого слова new и забывайте об этом.
Программирование в среде с автоматической сборкой мусора значительно облегчает разработку приложений, обязанности по управлению памятью, по сути, сняты с плеч программиста и возложены на CLR-среду.
Далее на качественном уровне – о сборке мусора, видах ресурсов, финализаторах и деструкторах (для начинающих — не самая актуальная тема, но для полноты изложения).

При работе с кучей выделяют управляемые и неуправляемые объекты. Массив, объявленный через ключевое слово new, является примером управляемого ресурса. Примеры неуправляемых ресурсов — это низкоуровневые файловые дескрипторы, низкоуровневые неуправляемые соединения с базами данных, фрагменты неуправляемой памяти. Финализатор (finalizer), как член-функция класса полезен только при использовании неуправляемых ресурсов

Как работает сборщик мусора?

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

Во время процесса сборки мусора исполняющая среда будет исследовать объекты в управляемой куче, чтобы определить, являются ли они по-прежнему достижимыми (т.е. корневыми) для приложения. Для этого среда CLR будет создавать графы объектов, представляющие все достижимые для приложения объекты в куче.

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

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

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

Поколения объектов

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

Поколение О Идентифицирует новый только что размещенный объект, который еще никогда не помечался как подлежащий удалению в процессе сборки мусора. Поколение 1 Идентифицирует объект, который уже «пережил» один процесс сборки мусора (был помечен как подлежащий удалению в процессе сборки мусора, но не был удален из-за наличия достаточного места в куче). Поколение 2 Идентифицирует объект, которому удалось пережить более одного прогона сборщика мусора.

Сборщик мусора сначала анализирует все объекты, которые относятся к поколению 0. Если после их удаления остается достаточное количество памяти, статус всех остальных (уцелевших) объектов повышается до поколения 1.

Если все объекты поколения 0 уже были проверены, но все равно требуется дополнительное пространство, проверяться на предмет достижимости и подвергаться процессу сборки мусора начинают объекты поколения 1. Объектам поколения 1, которым удалось уцелеть после этого процесса, затем назначается статус объектов поколения 2. Если же сборщику мусора все равно требуется дополнительная память, тогда на предмет достижимости начинают проверяться и объекты поколения 2. Объектам, которым удается пережить сборку мусора на этом этапе, оставляется статус объектов поколения 2, поскольку более высокие поколения просто не поддерживаются.

Из всего вышесказанного важно сделать следующий вывод: из-за отнесения объектов в куче к определенному поколению, более новые объекты (вроде локальных переменных) будут удаляться быстрее, а более старые (такие как объекты приложений) — реже.

Метод System.Object.Finalize() и финализатор

Назначение этого метода в C# примерно соответствует деструкторам С++, и он вызывается при сборке мусора для очистки ресурсов, занятых ссылочным объектом. Реализация Finalize() из Object на самом деле ничего не делает и игнорируется сборщиком мусора. Обычно переопределять Finalize() необходимо, если объект владеет неуправляемыми ресурсами, которые нужно освободить при его уничтожении. Сборщик мусора не может сделать это напрямую, потому что он знает только об управляемых ресурсах, поэтому полагается на финализацию, определенную вами.

В редких случаях, когда создается класс C#, в котором используются неуправляемые ресурсы, очевидно, понадобится обеспечить предсказуемое освобождение соответствующей памяти. Для этого необходимо переопределить метод Finalize().

Вместо этого для достижения того же эффекта должен применяться синтаксис деструктора (подобно С++). Объясняется это тем, что при обработке синтаксиса финализатора компилятор автоматически добавляет в неявно переопределяемый метод Finalize() приличное количество требуемых элементов инфраструктуры.

Класс System.GC (Garbage Collector)

В библиотеках базовых классов доступен класс по имени System.GC, который позволяет программно взаимодействовать со сборщиком мусора за счет обращения к его статическим членам. Необходимость в непосредственном использовании этого класса в разрабатываемом коде возникает крайне редко (а то и вообще никогда). Обычно единственным случаем, когда нужно применять члены System.GC. является создание классов, предусматривающих использование на внутреннем уровне неуправляемых ресурсов.

Метод Collect() заставляет сборщик мусора провести сборку мусора. Должен быть перегружен так, чтобы указывать, объекты какого поколения подлежат сборке, а также какой режим сборки использовать (с помощью перечисления GCCollectionMode)

Для проектов с неуправляемым кодом особое значение имеют два следующих метода из класса GC: AddMemoryPressure() и RemoveMemoryPressure(). С их помощью указывается большой объем неуправляемой памяти, выделяемой или освобождаемой в программе.

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

Сборщик мусора .NET предназначен в основном для того, чтобы управлять памятью вместо разработчиков. Однако в очень редких случаях требуется принудительно запустить сборку мусора с помощью метода GC.Collect(). Примеры таких ситуаций:
1) Приложение приступает к выполнению блока кода, прерывание которого возможным процессом сборки мусора является недопустимым.
2) Приложение только что закончило размещать чрезвычайно большое количество объектов и нуждается в как можно скорейшем освобождении большого объема памяти.

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

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

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

Оба подхода можно комбинировать и применять вместе в определении одного класса, получая преимущества от обеих моделей. Если пользователь объекта не забыл вызвать метод Dispose(), можно проинформировать сборщик мусора о пропуске финализации, вызвав метод GC.SuppressFinalize(). Если же пользователь забыл вызвать этот метод, объект рано или поздно будет подвергнут финализации и получит возможность освободить внутренние ресурсы. Преимущество такого подхода в том, что при этом внутренние ресурсы будут так или иначе, но всегда освобождаться.

Конструкция Using в C#

(часто встречается при работе с баами данных)

Ключевое слово using упрощает работу с объектами которые реализуют интерфейс IDisposable.
Интерфейс IDisposable содержит один метод .Dispose(), который используется для освобождения ресурсов,
которые захватил объект. При использовании Using не обязательно явно вызывать .Dispose() для объекта.

Пример

При этом компилятор фактически генерирует следующий код:

Заметим, что Using-блоки делают код более читабельным и компактным (некоторый аналог лямбда-операторов).

UncaughtException

Концепции программирования: Сборка мусора

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

Что такое сборка мусора?

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

Когда вы выделяете память, например, создавая переменную, данные попадают либо в стек, либо в кучу (чтобы лучше разобраться, чем они отличаются, читайте статью «Что такое стек и куча»). Когда вы выделяете участок памяти в стеке, вы всегда указываете локальную область видимости, точно зная нужный размер блока памяти. В стек попадают примитивные типы данных, массивы заданного размера и прочее. Стек — автономное хранилище памяти. С ним вам ни о чём не нужно беспокоиться: стек сам оперативно выделяет и чистит память. Для других переменных — объектов, буферов, строк или глобальных переменных — память выделяется в куче.

В отличие от стека куча не автономна. Переменная, отправленная на хранение в кучу, останется там на протяжении всей работы программы и может менять состояние, пока вы вручную не освободите участки памяти, которые больше не нужны. Сборщик мусора — это инструмент, который снимает с разработчика бремя ручного управления кучей. Большинство современных языков, например, Java, .NET framework, Python, Ruby или Go используют сборку мусора. А вот C и C++ нет. В этих языках над разработчикам висит чрезвычайно важная задача собственноручно вычищать память.

Зачем нужна сборка мусора?

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

Допустим, утечек памяти у вас нет. Не расслабляйтесь. Что если вы попытаетесь дать ссылку на переменную, которую уже удалили или переместили? Такая ситуация называется висячий указатель. В лучшем случае в ответ вы получите околесицу и, надеюсь, вызовите ошибку проверки вскоре после того, как используете переменную. А в худшем поверх старой области памяти система запишет новые данные, которые будут выдавать на вид правильные (но логически неверные) ответы. Вы вообще не будете понимать, что творится. Именно такие ошибки памяти сложнее всего устранять.

Вот почему сборщики мусора так необходимы. Они разберутся со всеми перечисленными проблемами. Да, работают они не идеально: сборка мусора забирает ресурсы компьютера и часто не так эффективна, чем ручное удаление памяти. Но это достойный инструмент, если вспомнить, от каких бед он спасает.

Как и когда запускается сборка мусора?

Зависит от алгоритма. Не существует какого-то одного раз и навсегда установленного способа собирать мусор. Как и компиляторы с интерпретаторами, механизмы сборки мусора совершенствуются со временем. Иногда сборщик мусора работает с заранее определенными временными интервалами, а иногда он ждёт определенных условий, которые должны возникнуть до его запуска. Сборщик мусора почти всегда работает в отдельном потоке в тандеме с программой. В зависимости от реализации языка программирования сборщик мусора либо приостанавливает программу, чтобы разом вымести весь мусор (такую остановку называют stop-the-world или пусть-весь-мир-подождет), либо удаляет память небольшими партиями, либо работает параллельно с программой.

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

Алгоритмы сборки мусора

Алгоритмов существует превеликое множество. Я приведу лишь самые распространенные, с которыми вы неизбежно столкнётесь. Заметьте, многие стандартные алгоритмы сборки мусора строятся друг на друге.

Подсчёт ссылок

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

Одно из важных преимуществ сборки мусора с помощью подсчёта ссылок заключается в том, что системе всегда понятно, когда есть мусор (когда на счётчике ноль). Но есть у этого способа и крупные минусы. Циклические ссылки просто-напросто невозможно вычистить. Если объект А ссылается на объект B, а объект В — обратно на А, сборщик мусора, работающий по принципу подсчёта ссылок, их никогда в жизни не удалит. А ещё подсчёт ссылок — очень неэффективный с точки зрения производительности метод, потому что по каждому объекту памяти постоянно идет работа счётчиков.

Из-за указанных недостатков современные сборщики мусора построены на других алгоритмах.

Пометка-очистка (Mark-Sweep)

Пометка-очистка, как и почти все остальные способы сборки мусора, не считая подсчёта ссылок, является отслеживающим алгоритмом. Суть таких алгоритмов — отслеживать объекты, доступные из одного или нескольких “корней”, с целью выявить недоступные (и, следовательно, неиспользуемые) области памяти. В отличие от подсчёта ссылок при пометке-очистке не производится постоянная проверка, а значит, этот алгоритм теоретически может выполняться в любой момент.

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

Пометка-очистка эффективнее подсчёта ссылок, потому что тут не нужно следить за счетчиками. А ещё такой метод позволяет удалять циклические ссылки. Однако примитивная пометка-очистка — это классический пример сборки мусора по принципу stop-the-world: пока выполняется алгоритм, вся программа вынуждена приостановить работу (не примитивные алгоритмы собирают мусор частями или параллельно с программой). Поскольку отслеживающая сборка мусора может осуществляться в любое время, вам не удастся предугадать, когда программа в очередной раз встанет. Кроме того, алгоритм дважды перебирает память кучи, а это еще больше замедляет программу. А еще при пометке-очистке не приводится в порядок фрагментированная память. Приведу наглядную аналогию. Представьте, что динамическая память (куча) — это цельная сетка. После метода пометки-очистки она будет выглядеть, как очень неудачная игра в Тетрис. Из-за фрагментации впоследствии снижается эффективность выделения памяти в куче. Так что этот алгоритм еще предстоит оптимизировать.

Пометка-сжатие (Mark-Compact)

Алгоритмы типа пометка-сжатие используют ту же логику, что пометка-очистка, с добавлением ещё как минимум одного действия с отмеченной областью памяти. Делается это, чтобы сжать и дефрагментировать память. Такой метод решают проблему с фрагментацией, которая произошла в результате пометки-очистки. В результате выделение памяти происходит эффективнее — по принципу выбивания (bump allocation), который похож на работу стека («последним пришёл — первым вышел»). С другой стороны, из-за дополнительных итераций пометка-сжатие выполняется и обрабатывается дольше.

Копирование

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

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

Поколения объектов

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

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

Заключение

Сборка мусора — не самая легкая для понимания тема, в которой и не обязательно досконально разбираться, если вы работаете с современными языками программирования. Но это недостаточно веский довод, чтобы проигнорировать эту тему. Да, сборка мусора почти не имеет отношения к коду, который вы пишете, но этот процесс — неотъемлемая часть любой языковой реализации. А алгоритм сборки мусора, используемый в языке, часто становится основной причиной, по которой люди либо любят, либо терпеть не могут некоторые реализации.

Здорово, если вы дочитали эту статью до конца. Надеюсь, вы узнали что-то новое. Если тема заинтересовала, рекомендую продолжить изучать сборку мусора. Вот увлекательный ресурс (на английском), с которого можно начать. Там есть гифки, иллюстрирующие работы разных алгоритмов сборки мусора.

Пока я собирал информацию для этой статьи, я заметил любопытную вещь. Большинство заметок на тему сборки мусора описывают этот процесс исключительно применительно к основной реализации Java. Конечно, сборка мусора встречается не только в Java, но думаю, такая гегемония именно этого языка в статьях про сборку мусора вызвана тем, что Java очень часто сравнивают с C++, где сборки мусора вообще нет. Надеюсь, что в будущем появится больше постов о том, как сборка мусора осуществляется в других языка. Пока же приходится довольствоваться тем, что есть!

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

Наткнулся на такие строки и прошу пояснить, что значит.

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

In many languages, resource management is primarily delegated to a garbage collector. C++
also offers a garbage collection interface so that you can plug in a garbage collector

На чем основывается этот интерфейс? И как его подключить. (это про сторонние библиотеки что ли?)

Сборщик мусора
Доброго вам времени суток! У меня вопрос можно ли автоматизировать удаление объектов размещаемых в.

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

Сборщик системной информации в Windows 8.1
люди, помогите написать программу которая будет работать под windows 8.1 и выводить информацию о.

Очистка мусора
1. Если я на форме, в процессе работы, буду очень часто менять изображения на одном и том же.

а дальше есть текст? может примеры будут.

Источник:
Язык программирования C++. Краткий курс 2 издание, глава 5.3. Управление ресурсами.

Добавлено через 7 минут

наверно про то что С++ также предлагает интерфейс это действительно умные указатели, а про подключить сборщик мусора это стороннюю библиотеку.

Добавлено через 1 минуту
хз надеюсь что кто нибудь лучше толканет.

так если речь про умные указатели, то зачем для них подключать стороннюю библиотеку когда есть стандартная?

Добавлено через 34 секунды
хотя если есть большое желание можно подтянуть буст и думать что все сделано как сказано в книжке ))

Решение

Нет, не про них речь.

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

Undisputed,
unique_ptr
shared_ptr

в посте #8 есть ссылка

Алексей1153,
Так DrOffset же пояснил что эта тема не про смарт принтеры

Добавлено через 22 минуты
_stanislav,
Чуточку вроде бы разобрался. Короче по идее если есть поддержка сборщика, то он время от времени запускается (возможно, в отдельном потоке), и «чистит» те адреса которые на его «взгляд» пора чистить. Например, если присвоить указателю nullptr. При этом надо быть осторожным. То есть если ты куда то скопировал этот адрес до присвоения исходной переменной nullptr, то копия этого адреса уже может указывать на просроченный участок. Если таких проблем хочется избежать как я понял надо применять к такому адресу std::declare_reachable. Это кстати и есть часть интерфейса gc (остальные функции интерфейса есть по ссылке выше на ФАГ Страуструпа). Но это все как я понял толком нигде не реализовали и стандарт как бы не настаивает на этом.

DrOffset,
Поправь плиз если я ошибаюсь

Добавлено через 10 минут
Алексей1153,
А что касается ссылки из 8 поста то hoggy указал подраздел который нужно смотреть. В
Этом подразделе явно нет смарт поинтеров

Читать еще:  Lenovo ideapad 330 загрузка с флешки
Ссылка на основную публикацию
Adblock
detector