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

Материал из Википедии — свободной энциклопедии
Перейти к: навигация, поиск

В программировании сборка мусора (устоявшийся термин,[1] с точки зрения русского языка правильнее «сбор мусора»,[2] англ. garbage collection, GC) — одна из форм автоматического управления памятью. Специальный процесс, называемый сборщиком мусора (англ. garbage collector), периодически освобождает память, удаляя объекты, которые уже не будут востребованы приложениями.

Если бы память компьютера была бесконечной, можно было бы просто оставлять ненужные объекты в памяти. Сбор мусора — эмуляция такого бесконечного компьютера на конечной памяти.[3] Многие ограничения сборщиков мусора (нет гарантии, что финализатор выполнится; управляет только памятью, но не другими ресурсами) вытекают из этой метафоры.

История[править | править вики-текст]

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

В промышленных процедурных и объектных языках сборка мусора долго не использовалась. Предпочтение отдавалось ручному управлению памятью, как более эффективному и предсказуемому. Но со второй половины 1980-х годов технология сборки мусора стала использоваться и в директивных (императивных), и в объектных языках программирования, а со второй половины 1990-х годов всё большее число создаваемых языков и сред, ориентированных на прикладное программирование, включают механизм сборки мусора либо как единственный, либо как один из доступных механизмов управления динамической памятью. В настоящее время она используется в Oberon, Java, Python, Ruby, Perl, C#, D, F# и других языках.

Проблемы ручного управления памятью[править | править вики-текст]

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

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

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

Висячая ссылка (англ. dangling pointer)
Висячая ссылка — это оставшаяся в использовании ссылка на объект, который уже удалён. После удаления объекта все сохранившиеся в программе ссылки на него становятся «висячими». Память, занимаемая ранее объектом, может быть передана операционной системе и стать недоступной, или быть использована для размещения нового объекта в той же программе. В первом случае попытка обратиться по «повисшей» ссылке приведёт к срабатыванию механизма защиты памяти и аварийной остановке программы, а во втором — к непредсказуемым последствиям.
Появление висячих ссылок обычно становится следствием неправильной оценки времени жизни объекта: программист вызывает команду удаления объекта до того, как его использование прекратится.
Утечка памяти (англ. memory leak)
Создав объект в динамической памяти, программист может не удалить его после завершения использования. Если ссылающейся на объект переменной будет присвоено новое значение и на объект нет других ссылок, он становится программно недоступным, но продолжает занимать память, поскольку команда его удаления не вызывалась. Такая ситуация и называется утечкой памяти.
Если объекты, ссылки на которые теряются, создаются в программе постоянно, то утечка памяти проявляется в постепенном увеличении объёма используемой памяти; если программа работает долго, объём используемой ею памяти постоянно растёт, и через какое-то время ощутимо замедляется работа системы (из-за необходимости при любом выделении памяти использовать свопинг), либо программа исчерпывает доступный объём адресного пространства и завершается с ошибкой.

Механизм сборки мусора[править | править вики-текст]

Основные принципы[править | править вики-текст]

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

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

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

Достижимость объекта[править | править вики-текст]

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

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

Неформально можно задать следующее рекурсивное определение достижимого объекта:

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

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

Алгоритм выставления флагов

Простой алгоритм определения достижимых объектов, «алгоритм пометок» (Mark and Sweep), заключается в следующем:

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

(Следует обратить внимание, что, согласно данному алгоритму, если два или более объектов ссылаются друг на друга, но ни на один из этих объектов нет других ссылок, то имеет место циклическая ссылка, и вся группа считается недостижимой. Эта особенность алгоритма позволяет гарантированно удалять группы объектов, использование которых прекратилось, но в которых имеются ссылки друг на друга. Такие группы часто называются «islands of isolation» (острова изоляции))

Алгоритм подсчёта ссылок

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

Стратегии сборки мусора[править | править вики-текст]

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

Обе стратегии имеют как достоинства, так и недостатки

Скорость выделения и освобождения памяти
Неперемещающий сборщик мусора быстрее освобождает память (поскольку эта операция сводится к пометке соответствующих блоков памяти как свободных), но тратит больше времени на её выделение (поскольку память фрагментируется, и при выделении необходимо найти в памяти нужное количество блоков подходящего размера).
Перемещающий сборщик требует сравнительно больше времени при сборе мусора (тратится дополнительное время на дефрагментацию памяти и изменение всех ссылок на перемещаемые объекты), зато перемещение позволяет использовать чрезвычайно простой и быстрый (O(1)) алгоритм выделения памяти. При дефрагментации объекты передвигаются так, чтобы разделить всю память на две большие области — занятую и свободную, и сохраняется указатель на их границу. Для выделения новой памяти достаточно лишь переместить эту границу, вернув кусок из начала свободной памяти.
Скорость доступа к объектам в динамической памяти
Объекты, поля которых используются совместно, перемещающий сборщик позволяет размещать в памяти недалеко друг от друга. Тогда они вероятнее окажутся в кэше процессора одновременно, что уменьшит количество обращений к относительно медленному ОЗУ.
Совместимость с инородным кодом
Перемещающий сборщик мусора вызывает затруднения при использовании кода, который не контролируется системой автоматического управления памятью (такой код называется инородным (англ. foreign) в традиционной терминологии или неуправляемым (англ. unmanaged) в терминологии Microsoft). Указатель на память, выделенную в системе с неперемещающим сборщиком, можно просто передать инородному коду для использования, удерживая хотя бы одну обычную ссылку на объект, чтобы сборщик его не удалил. Перемещающий сборщик меняет положение объектов в памяти, синхронно меняя все ссылки на них, но поменять ссылки в инородном коде он не может, в результате переданные инородному коду ссылки после перемещения объекта станут некорректными. Для работы с инородным кодом используются различные специальные приёмы, например, pinning — явная блокировка объекта, запрещающая его перемещение во время сборки мусора.

Поколения объектов[править | править вики-текст]

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

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

Другие механизмы[править | править вики-текст]

Неизменные объекты (англ. immutable objects)
Например, java.lang.String в Java. Как только строке присвоили какое-то значение, её уже нельзя менять. Подпрограммы будут передавать ссылку на эту строку друг другу, и когда все ссылки исчезнут, строка будет уничтожена сборщиком мусора.
Финализаторы
Финализатор указывает, что делать, когда объект попадает под сборщик мусора. Обычно финализаторы пишут для обёрток над объектами операционной системы (файлами, сетевыми сокетами); они автоматически закрывают соответствующий объект.
Поскольку объект до сбора может «провисеть» в памяти довольно долго, хорошая манера программирования — закрыть файл или сокет вручную, командой наподобие close().

Требования к языку и системе[править | править вики-текст]

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

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

Проблемы использования[править | править вики-текст]

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

Освобождение других ресурсов, занятых объектом
Помимо динамической памяти, объект может владеть и другими ресурсами — подчас более ценными, чем память. Если объект при создании открывает файл, по завершении использования он должен его закрыть, если подключается к СУБД — должен отключиться. В системах с ручным управлением памятью это делается непосредственно перед удалением объекта из памяти, чаще всего — в деструкторах соответствующих объектов. В системах со сборкой мусора обычно есть возможность выполнить некоторый код непосредственно перед удалением объекта, так называемые финализаторы, но для освобождения ресурсов они не годятся, так как момент удаления заранее неизвестен, и может оказаться, что ресурс освобождается намного позже, чем перестаёт использоваться объект. В подобных случаях программисту всё равно приходится отслеживать использование объекта вручную и вручную выполнять операции по освобождению занятых объектом ресурсов. В C# специально для этой цели существует интерфейс IDisposable, в Java — Closeable.
Утечка памяти
В системах со сборкой мусора тоже могут возникать утечки памяти, правда, имеющие несколько другую природу. Ссылка на неиспользуемый объект может сохраниться в другом объекте, который используется и становится своеобразным «якорем», удерживающим ненужный объект в памяти. Например, созданный объект добавляется в коллекцию, используемую для вспомогательных операций, потом перестаёт использоваться, но не удаляется из коллекции. Коллекция удерживает ссылку, объект остаётся достижимым и не подвергается сборке мусора. Результатом становится всё та же утечка памяти.
Чтобы устранить подобные проблемы, среда исполнения может поддерживать специальное средство — так называемые слабые ссылки.

Достоинства и недостатки[править | править вики-текст]

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

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

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

Поддержка в некоторых императивных языках автоматического вызова деструктора (C++, Ада, Delphi (через интерфейсы)), а также более простая, чем сборка мусора, технология использования «умных ссылок» (отслеживания количества ссылок на объект непосредственно в нём и автоматическое удаление при удалении последней ссылки, как это сделано в технологии COM) значительно снижает вероятность утечек, позволяя концентрировать опасные места внутри реализации класса, при этом не требуя лишних ресурсов, хотя и требует при этом более высокой квалификации программиста. Конечно, писать код освобождения ресурсов всё равно придётся, но автоматические деструкторы дадут уверенность, что этот код обязательно вызовется. Впрочем, для наиболее часто используемых ресурсов во всех популярных языках, поддерживающих автодеструкторы, уже есть автоматические обёртки, которые сами закрывают ресурс, благодаря чему забота о ресурсах становится едва ли не проще, чем с непредсказуемым сборщиком мусора.

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

Управление памятью в конкретных языках и системах[править | править вики-текст]

Сборка мусора часто противопоставляется ручному управлению памятью, при котором программист явно указывает, когда и какие области памяти надо освободить. Однако есть языки, в которых используется комбинация двух методов управления памятью, равно как есть и другие технологии решения той же фундаментальной проблемы (например, en:region inference).

Некоторые языки программирования требуют использования механизма сборки мусора в соответствии со своей спецификацией (Java, C#, Eiffel), другие — по причинам эффективности реализации (например, формальные языки для лямбда-исчисления) — эти языки называются языками со сборкой мусора. Многие языки со сборкой мусора не имеют возможностей для явного ручного удаления объектов (например, оператора delete), благодаря чему возникновение висячих ссылок исключается в принципе, а сборщик мусора лишь занимается удалением объектов, на которые нет ссылок из программы.

Некоторые языки (например, Modula-3) позволяют использовать как ручное управление памятью, так и сборку мусора в одном приложении — используя две отдельные кучи.

См. также[править | править вики-текст]

Примечания[править | править вики-текст]