Letysite.ru

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

Сборщик мусора 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-блоки делают код более читабельным и компактным (некоторый аналог лямбда-операторов).

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

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

Назначение сборщика мусора

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

При создании сборщика мусора в .NET преследовались две основные цели:

избавиться от ошибок и ловушек, связанных с управлением памятью вручную;

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

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

Управление свободным списком

Управление на основе списка свободных блоков — это механизм управления распределением памяти в стандартной библиотеке языка C, который также по умолчанию используется функциями управления памятью в C++, такими как new и delete. Это детерминированный диспетчер памяти, при использовании которого вся ответственность за выделение и освобождение памяти ложится на плечи разработчика. Свободные блоки памяти хранятся в виде связанного списка, откуда изымаются блоки памяти при выделении, и куда они возвращаются, при освобождении.

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

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

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

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

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

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

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

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

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

Сборка мусора на основе подсчета ссылок

Сборщик мусора, опирающийся на подсчет ссылок, связывает каждый объект с целочисленной переменной — счетчиком ссылок. В момент создания объекта счетчик ссылок инициализируется значением 1. Когда приложение создает новую ссылку на объект, его счетчик ссылок увеличивается на 1. Когда приложение удаляет ссылку на существующий объект, его счетчик ссылок уменьшается на 1. Когда счетчик ссылок достигает значения 0, объект можно уничтожить и освободить занимаемую им память.

Одним из примеров управления памятью на основе подсчета ссылок в экосистеме Windows является объектная модель программных компонентов (Component Object Model, COM). Объекты COM снабжаются счетчиками ссылок, определяющими продолжительность их существования. Когда значение счетчика ссылок достигает 0, объект может освободить занимаемую им память. Основное бремя подсчета ссылок ложится на плечи разработчика, в виде явного вызова методов AddRef() и Release(), хотя в большинстве языков имеются обертки, автоматизирующие вызовы этих методов при создании и удалении ссылок.

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

Высокая стоимость управления: всякий раз, когда создается или уничтожается ссылка на объект, необходимо обновлять счетчик ссылок. Это означает, что к стоимости обновления ссылки прибавляются накладные расходы на выполнение таких тривиальных операций, как присваивание ссылки (a = b) или передача ссылки в функцию по значению. В многопроцессорных системах выполнение таких операций требует применения механизмов синхронизации и вызывает «пробуксовку» кеша процессора, при попытке нескольких потоков выполнения одновременно изменить счетчик ссылок.

Использование памяти: счетчик ссылок на объект должен храниться в памяти объекта. Это на несколько байтов увеличивает объем памяти, занимаемой объектом, что делает подсчет ссылок нецелесообразным для легковесных объектов. (Впрочем, это не такая большая проблема для CLR, где к каждому объекту «в нагрузку» добавляется от 8 до 16 байт.)

Правильность: при управлении памятью на основе подсчета ссылок возникает проблема утилизации объектов с циклическими ссылками. Если приложение больше не ссылается на некоторую пару объектов, но каждый из них продолжает хранить ссылку на другой объект (как показано на рисунке ниже), возникает утечка памяти. Эта проблема описывается в документации COM, где явно оговаривается, что такие циклические ссылки должны уничтожаться вручную. Другие платформы, такие как язык программирования Python, поддерживают дополнительные механизмы определения циклических ссылок и их устранения, применение которых влечет за собой увеличение стоимости сборки мусора.

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

Разбираемся с Garbage Collection в C#

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

Garbage Collection

Специальный механизм, называемый сборщиком мусора (garbage collector), периодически освобождает память, удаляя объекты, которые уже не будут востребованы приложением- то есть производит «сбор мусора».

Сборка мусора была впервые применена Джоном Маккарти в 1959 году в среде программирования на разработанном им функциональном языке программирования Lisp. Впоследствии она применялась в других системах программирования и языках, преимущественно — в функциональных и логических.

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

Для доступа к ресурсу вам нужно:

  • Выделить память для типа, представляющего ресурс
  • Инициализировать выделенную память, установив начальное состояние ресурса.
  • Использовать ресурс, обращаясь к членам его типа
  • Ликвидировать состояние ресурса
  • Освободить память

Сборка мусора (garbage collection) полностью освобождает разработчика от необходимости следить за использованием и своевременным освобождением памяти.

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

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

  • подсчитывает количество байтов, необходимых для размещения полей типа
  • прибавляет к полученному значению количество байтов, необходимых для размещения системных полей объекта.
  • Если в управляемой куче достаточно места для объекта, ему выделяется память, начиная с адреса, на который ссылается указатель NextObjPtr, а занимаемые им байты обнуляются.
  • Вызывается конструктор типа, и IL-команда newobj возвращает адрес объекта (также перемещается NextObjPtr)

При вызове оператора new в области, выделяемой под объект, может не хватать свободного адресного пространства. Куча выясняет объем недостающей памяти и добавляет байты, необходимые для объекта, к адресу, заданному указателем NextObjPtr . Если результирующее значение выходит за пределы адресного пространства, значит, куча заполнена и следует выполнить сборку мусора. Сборщик мусора проверяет наличие в куче больше не используемых приложением объектов, чтобы освободить занятую ими память. Сборщик переходит к этапу сборки мусора, называемому маркировкой (maгking). Он проходит по стеку потока и проверяет все корни маркируя объекты. После маркировки корня и объекта, сборщик мусора проверяет следующий корень и продолжает маркировать объекты. Встретив уже маркированный объект, сборщик мусора останавливается. Затем сборщик переходит к следующему этапу сборки мусора, называемому сжатием. Теперь он проходит кучу линейно в поисках непрерывных блоков не маркированных объектов, то есть мусора. Небольшие блоки сборщик не трогает, а в больших непрерывных блоках он перемешает вниз все не мусорные объекты, сжимая при этом кучу.
Куча организована в виде поколений, что позволяет ей обрабатывать долгоживущие и короткоживущие объекты. Сборка мусора в основном сводится к уничтожению короткоживущих объектов, которые обычно занимают только небольшую часть кучи. В куче существует три поколения объектов.

  • Поколение 0. Объекты не проверялись сборщиком мусора. Это самое молодое поколение содержит короткоживущие объекты. Примером короткоживущего объекта является временная переменная. Сборка мусора чаще всего выполняется в этом поколении. Вновь распределенные объекты образуют новое поколение объектов и неявно являются сборками поколения 0, если они не являются большими объектами, в противном случае они попадают в кучу больших объектов в сборке поколения 2.
  • Поколение 1. Объекты пережившие одну проверку сборщиком мусора (а также объекты помеченные на удаление, но не удаленные, так как в управляемой куче было достаточно свободного места). Это поколение содержит коротко живущие объекты и служит буфером между короткоживущими и долгоживущими объектами.
  • Поколение 2. Объекты, которые пережили более чем одну проверку сборщиком мусора. Это поколение содержит долгоживущие объекты. Примером долгоживущих объектов служит объект в серверном приложении, содержащий статические данные, которые существуют в течение длительности процесса.

Objective-C: управление памятью и сборщики мусора10

“Чем меньше кода пишет программист, тем меньше в коде ошибок” – сказал консультант главы Apple Computer Стив Джобс на январском MacWorld Expo 1997 года. Во второй половине нулевых выяснилось, что это правило работает не всегда… В 2006 Крис Латнер, один из руководителей и ключевых разработчиков LLVM, задумался над исправлением недостатков Objective-C. Сначала это было чем-то вроде хобби. Крис обратился к участникам нескольких профильных “почтовых” форумов с вопросом “Что бы вы улучшили в Objective-C?”. Ваш покорный слуга тоже внес свою лепту.

В том году Крис стал начальником одной из групп в отделении средств разработки Apple.

Обсуждение, временами переходящее в ожесточенные встречные бои, продолжалось и после выхода в свет Objective-C 2.0 – но на этом экскурс в историю Objective-C прервем. Про этот язык, и про “Objective-C 3.0”, который без “C” и называется иначе, поговорим в отдельной серии.

Сообщу только один малоизвестный факт: это был не первый Objective-C 2.0 в истории, в начале 90-х, когда NeXT приобрела права на торговый знак и на язык, её специалисты всерьез занялись доработкой и улучшением языка. Заменили “тормозную” среду времени исполнения на собственную (неплохую), посадили язык на GCC C (считавшийся лучшим в то время).

Для удобства, релизы с очередными новшествами обозначали версиями. Были не только 2.0 и, вроде бы, даже 2.1 – кончилось все не то на Objective-C 4.1, не то на 5.3. К 1993 или 1994 году бурное развитие языка прекратилось, и он снова стал “просто Objective-C”.

В 2006 году был объявлен Objective-C 2.0 третьего тысячелетия, в 2007 он пришел на смену просто Objective-C. Будущая iOS была написана, большей частью, на Objective-C 2.0.

В Objective-C 2.0 было много новшеств, но одно из них было одновременно благом и злом. Система управления памятью осталась той же, что и раньше – но теперь приличную часть кода подставлял компилятор…

Это продолжение серии про WWDC 2011, предыдущие части здесь:

Почему в Objective-C нет сборщика мусора?

Этот вопрос занимал многих. До великой мобильной революции 2008 года Objective-C был экзотикой для большей части программирующего населения. Те, кому все же пришлось с ним познакомиться (мимолетно), его ненавидели. Самой ненавистной особенностью языка было обилие квадратных скобок. Недели через две к ним привыкаешь, это не проблема вообще – но ненависти не прикажешь.

Второй в шкале ненависти была система управления памяти. Строго говоря, она не была частью языка – это был “грех” объектно-ориентированных библиотек, и обладала массой достоинств, но её освоение стояло прочной каменной стеной на пути неофитов. “Почему в Objective-С нет встроенного сборщика мусора?” – спрашивали они, в тысячный раз воюя с непослушными и загадочными retain/release/autorelease – “Ведь существуют десятки его реализаций с открытым исходным кодом?”

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

Система управления памятью в Objective-C (забудем про библиотеки) была самой лучшей в мире. Не без недостатков, но где их нет? Славное было времечко.

Потому и не было: чувствующие систему управления памятью и отлично владеющие ей специалисты (а другим лучше вообще не браться за сооружение сборщика мусора в чужом и непонятном языке), не видели в этом необходимости. А преодоление порога R/R/A даже полезно: у тех кто смог, повышается самооценка.

Но это моё личное мнение.

Преступление Objective-C 2.0

В улучшенном Objective-C было много новшеств, например инициализация неизменяемых коллекций литералами.

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

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

К восторгу неофитов, все работало. Никаких препятствий, которые нужно преодолевать, не было… казалось бы. На самом деле, система управления памяти была той же самой, и синтаксический сахар слетал с неё, рано или поздно – но всегда. Никто в здравом уме и в трезвой памяти не сядет за штурвал сверхзвукового самолеты, надеясь на автопилот и не умея управлять самолетом вручную.

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

Без веры в свое оружие победить невозможно. Простите за банальность.

Пришлось проводить жесткий ликбез. Сколько неофитов (да, я избегаю слова “новичок”), столкнувшись с этим, плюнули и ушли? В честные жестокие времена неофиты знали что перед ними – опасный враг, с которым надо бороться, были начеку и делали значительно меньше глупостей.

Говорят, в первобытные времена в диком лесу взрослый человек запросто справлялся с целыми стаями крупных псовых. Современный человек – увы.

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

В том же году в Obj-C появился и сборщик мусора. Экспериментальный, на основе самого лучшего сборщика с открытым исходным кодом, доработанный инженерами Apple и самим автором (не за так, естественно) – мне не довелось иметь с ним дело, в iOS его не было. По понятным причинам.

Потом его еще раз доработали, потом еще – и в конце концов наступил 2011 год.

В течение какого-то времени после “закрытия” сборщика мусора, он поддерживался – а через год или два окончательно ушел в небытие.

О том, что случилось в 2011 году, читайте в продолжении

Обсудить историю Apple вы можете в нашем Telegram-чате.

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