В программировании сборка мусора[1] (англ. garbage collection) — одна из форм автоматического управления памятью. Специальный процесс, называемый сборщиком мусора (англ. garbage collector), периодически освобождает память, удаляя объекты, которые уже не будут востребованы приложениями.
Если бы память компьютера была бесконечной, можно было бы просто оставлять ненужные объекты в памяти. Сборка мусора — эмуляция такого бесконечного компьютера на конечной памяти[2]. Многие ограничения сборщиков мусора (нет гарантии, что финализатор выполнится; управляет только памятью, но не другими ресурсами) вытекают из этой метафоры.
Сборка мусора была впервые применена Джоном Маккарти в 1959 году в среде программирования на разработанном им функциональном языке программирования Лисп. Впоследствии она применялась в других системах программирования и языках, преимущественно — в функциональных и логических. Необходимость сборки мусора в языках этих типов обусловлена тем, что структура таких языков делает крайне неудобным отслеживание времени жизни объектов в памяти и ручное управление ею. Широко используемые в этих языках списки и основанные на них сложные структуры данных во время работы программ постоянно создаются, надстраиваются, расширяются, копируются, и правильно определить момент удаления того или иного объекта затруднительно.
В промышленных процедурных и объектных языках сборка мусора долго не использовалась. Предпочтение отдавалось ручному управлению памятью, как более эффективному и предсказуемому. Но со второй половины 1980-х годов технология сборки мусора стала использоваться и в директивных (императивных), и в объектных языках программирования, а со второй половины 1990-х годов всё большее число создаваемых языков и сред, ориентированных на прикладное программирование, включают механизм сборки мусора либо как единственный, либо как один из доступных механизмов управления динамической памятью. В настоящее время она используется в Oberon, Java, Python, Ruby, C#, D, F#, Go и других языках.
Традиционным для директивных языков способом управления памятью является ручной. Его сущность в следующем:
В любом языке, допускающем создание объектов в динамической памяти, потенциально возможны две проблемы: висячие ссылки и утечки памяти.
Сборка мусора — технология, позволяющая, с одной стороны, упростить программирование, избавив программиста от необходимости вручную удалять объекты, созданные в динамической памяти, с другой — устранить ошибки, вызванные неправильным ручным управлением памятью.
В системе со сборкой мусора обязанность освобождения памяти от объектов, которые больше не используются, возлагается на среду исполнения программы. Программист лишь создаёт динамические объекты и пользуется ими, он может не заботиться об удалении объектов, поскольку это делает за него среда. Для осуществления сборки мусора в состав среды исполнения включается специальный программный модуль, называемый «сборщиком мусора». Этот модуль периодически запускается, определяет, какие из созданных в динамической памяти объектов более не используются, и освобождает занимаемую ими память.
Периодичность запуска сборщика мусора определяется особенностями системы. Сборщик может работать в фоновом режиме, запускаясь при неактивности программы (например, когда программа простаивает, ожидая ввода данных пользователем). Сборщик мусора запускается безусловно, останавливая выполнение программы (англ. Stop-the-world), когда очередную операцию выделения памяти оказывается невозможно выполнить из-за того, что вся доступная память исчерпана. После освобождения памяти прерванная операция выделения памяти возобновляется, и программа продолжает исполняться дальше. Если же оказывается, что освободить память невозможно, среда исполнения останавливает программу с сообщением об ошибке «Недостаточно памяти».
Хотя, в общем случае, невозможно точно определить момент, когда объект был использован в последний раз и больше не нужен, сборщики мусора используют консервативные оценки, позволяющие определить, что в будущем объект гарантированно не будет использоваться.
Обычно критерием того, что объект ещё используется, является наличие ссылок на него. Если в системе нет больше ссылок на данный объект, то он, очевидно, больше не может быть использован программой, а следовательно, может быть удалён. Этот критерий используется большинством современных сборщиков мусора и называется ещё достижимостью объекта.
Неформально можно задать следующее рекурсивное определение достижимого объекта:
Такое определение не является теоретически наилучшим, так как в число достижимых, согласно ему, попадают и те объекты, которые уже никогда не будут использованы, но на которые пока ещё существуют ссылки. Оптимальным было бы считать недостижимым объект, к которому в процессе дальнейшей работы программы не будет ни одного обращения, однако выявление таких объектов невозможно, поскольку сводится к алгоритмически неразрешимой задаче об остановке (для этого достаточно предположить, что некоторый объект X будет использован в том и только в том случае, если успешно завершится программа P).
Простой алгоритм определения достижимых объектов, «алгоритм пометок» (Mark and Sweep), заключается в следующем:
(Следует обратить внимание, что, согласно данному алгоритму, если два или более объектов ссылаются друг на друга, но ни на один из этих объектов нет других ссылок, то имеет место циклическая ссылка, и вся группа считается недостижимой. Эта особенность алгоритма позволяет гарантированно удалять группы объектов, использование которых прекратилось, но в которых имеются ссылки друг на друга. Такие группы часто называются «islands of isolation» (острова изоляции))
Другой вариант алгоритма определения достижимости — обычный подсчёт ссылок. Его использование замедляет операции присваивания ссылок, но зато определение достижимых объектов тривиально — это все объекты, значение счётчика ссылок которых превышает нуль. Без дополнительных уточнений этот алгоритм, в отличие от предыдущего, не удаляет циклически замкнутые цепочки вышедших из употребления объектов, сохранивших ссылки друг на друга.
Как только определено множество недостижимых объектов, сборщик мусора может освободить память, занимаемую ими, и оставить остальное как есть. Также можно после освобождения памяти переместить все или часть оставшихся объектов в другие области памяти, обновив вместе с этим все ссылки на них. Эти два варианта реализации называются, соответственно, неперемещающим и перемещающим.
Обе стратегии имеют как достоинства, так и недостатки
Как показывает практика, недавно созданные объекты чаще становятся недостижимыми, чем объекты, существующие длительное время. В соответствии с этой закономерностью многие современные сборщики мусора подразделяют все объекты на несколько поколений — серий объектов с близким временем существования. Как только память, выделенная одному из поколений, заканчивается, в этом поколении и во всех более «молодых» производится поиск недостижимых объектов. Все они удаляются, а оставшиеся переводятся в более «старое» поколение.
Использование поколений сокращает время цикла сборки мусора, поскольку уменьшается число просматриваемых в ходе сборки объектов, однако этот метод требует от среды исполнения отслеживания ссылок между разными поколениями.
"Hello"
, строка размещается в памяти и переменная получает ссылку на неё. Но если впоследствии такой же строкой будет инициализирована другая переменная, то система найдёт ранее созданную строку "Hello"
в памяти и присвоит второй переменной ссылку на неё же, вместо того, чтобы размещать строку в памяти повторно. Поскольку строка является принципиально неизменной, на логику работы программы такое решение никак не повлияет, но строка не будет дублироваться в памяти, сколько бы раз она ни использовалась. И только когда все ссылки на неё будут удалены, строка будет уничтожена сборщиком мусора.close()
, когда объект реально перестаёт использоваться.Чтобы программа могла использовать сборку мусора, необходимо выполнение ряда условий, относящихся к языку, среде исполнения и самой решаемой задаче.
Вопреки часто встречающимся утверждениям, наличие сборки мусора вовсе не освобождает программиста от всех проблем управления памятью.
IDisposable
, в Java — Closeable
.null
, как только объект исчезает — поэтому код должен быть готов к тому, что однажды ссылка укажет в никуда. String out = "";
// Предполагается, что strings содержит большое количество коротких строк,
// из которых нужно собрать одну большую строку в переменной out.
for( String str : strings ) {
out += str; // Данный код будет каждую итерацию создавать
// новую строковую переменную и выделять под неё память.
}
По сравнению с ручным управлением памятью сборка мусора безопаснее, поскольку она предотвращает утечки памяти и возникновение висячих ссылок из-за несвоевременного удаления объектов. Она упрощает и сам процесс программирования.
Считается, что сборка мусора заметно сокращает трудозатраты на управление памятью по сравнению с языками, где она не реализована. Согласно исследованию[3], программисты на Си тратят 30 % — 40 % общего времени разработки (не считая отладки) только на управление памятью. Впрочем, существуют исследования с противоположными выводами, например, в работе[4] утверждается, что реальная разница в скорости разработки программного обеспечения на C++, где нет автоматической сборки мусора, и на Java, где она реализована, невелика.
Наличие сборщика мусора у неопытного разработчика может создать ложное убеждение, что ему вообще не нужно уделять внимание управлению памятью. Хотя сборщик мусора действительно сокращает проблемы неправильного управления памятью, но не устраняет их полностью, а те, что сохраняются, проявляются не в очевидных ошибках, таких как общая ошибка защиты, а в неоправданном расходовании памяти при работе программы. Типичный пример: если программист упустил из виду, что на объект остался хотя бы один необнулённый указатель в глобальной области видимости, такой объект никогда не будет удалён; поиск такой псевдоутечки может быть очень сложным.
Зачастую критически важной является не только гарантия освобождения ресурса, но и гарантия того, что он освободится до вызова какой-то другой процедуры — например, открытые файлы, входы в критические секции. Попытки отдать управление этими ресурсами сборщику мусора (через финализаторы) будут неэффективны или даже некорректны, поэтому приходится управлять ими вручную. В последнее время даже в языках со сборщиком мусора вводят синтаксис, гарантирующий выполнение «кода очистки» (например, специального метода-«деструктора») при выходе переменной, ссылающейся на объект, из зоны видимости.
Во многих случаях системы со сборкой мусора демонстрируют меньшую эффективность, как по скорости, так и по объёму используемой памяти (что неизбежно, так как сборщик мусора сам потребляет ресурсы и нуждается в некотором избытке свободной памяти для нормальной работы). Кроме того, в системах со сборкой мусора сложнее реализуются низкоуровневые алгоритмы, требующие прямого доступа к оперативной памяти компьютера, поскольку свободное использование указателей невозможно, и прямой доступ к памяти требует наличия специальных интерфейсов, написанных на низкоуровневых языках. С другой стороны, в современных системах со сборкой мусора используются очень эффективные алгоритмы управления памятью, дающие минимальные накладные расходы. Нельзя также не учитывать тот факт, что сейчас оперативная память относительно дёшева и доступна. В таких условиях ситуации, когда критическими для эффективности программы становятся именно затраты на сборку мусора, крайне редки.
Существенное преимущество сборки мусора проявляется тогда, когда динамически созданные объекты живут длительное время, многократно дублируются, а ссылки на них передаются между различными участками программы. В таких условиях достаточно сложно определить место, где объект перестал использоваться и его можно удалять. Поскольку именно такая ситуация складывается при широком использовании динамически изменяемых структур данных (списки, деревья, графы), сборка мусора является необходимой в широко использующих такие структуры функциональных и логических языках, типа Хаскелла, Лиспа или Пролога. Использование сборки мусора в традиционных императивных языках (основанных на структурной парадигме, возможно, дополненной объектными средствами) определяется желаемым соотношением между простотой и скоростью разработки программ и эффективностью её выполнения.
Поддержка в некоторых императивных языках автоматического вызова деструктора (C++[5], Ада, Delphi), а также более простая, чем сборка мусора, технология использования «умных ссылок» (отслеживания количества ссылок на объект непосредственно в нём и автоматическое удаление при удалении последней ссылки, как это сделано в технологии COM) значительно снижает вероятность утечек, позволяя концентрировать опасные места внутри реализации класса, при этом не требуя лишних ресурсов, хотя и требует при этом более высокой квалификации программиста. Конечно, писать код освобождения ресурсов всё равно приходится, но автоматические деструкторы гарантируют, что этот код обязательно вызовется. Для наиболее часто используемых ресурсов во всех популярных языках, поддерживающих автодеструкторы, уже есть автоматические обёртки, которые сами закрывают ресурс, благодаря чему забота о ресурсах становится едва ли не проще, чем с непредсказуемым сборщиком мусора.
Ещё с 1960-х годов существует управление памятью на региональной основе[en] — технология, в которой память делится на относительно крупные фрагменты, называемые регионами, и уже внутри регионов память выделяется отдельным объектам. При ручном управлении регионы создаются и удаляются самим программистом, при автоматическом — используются различные виды консервативных оценок, позволяющие определить, когда все объекты, выделенные в пределах региона, перестают быть используемыми, после чего система управления памятью удаляет регион целиком. Например, создаётся регион, в котором выделяется память для всех объектов, создаваемых внутри определённой области видимости, не передаваемых вовне, и этот регион уничтожается одной командой, когда исполнение программы выходит из данной области видимости. Переход в управлении памятью (неважно — ручном или автоматическом) от отдельных объектов к более крупным единицам во многих случаях позволяет упростить учёт времени жизни объектов и одновременно снизить накладные расходы. Реализации (разной степени автоматизированности) регионального управления памятью существуют для многих языков программирования, включая ML, Пролог, Си, Cyclone.
Язык программирования Rust предлагает концепцию «владения», основанную на жёстком контроле со стороны компилятора соответствия времени жизни и области видимости объектов. Идея состоит в том, что при создании объекта переменная, которой присваивается ссылка на него, становится «владельцем» этого объекта, и область видимости переменной-владельца ограничивает время жизни объекта. При выходе из области видимости владельца объект автоматически удаляется. Путём присваивания ссылки на объект другой переменной он может быть «заимствован», но заимствование всегда временно и должно быть завершено в пределах времени жизни владельца объекта. «Владение» может быть передано другой переменной (например, объект может быть создан внутри функции и возвращён в её результате), но при этом исходный владелец теряет доступ к объекту. В совокупности правила построены так, чтобы гарантировать невозможность неконтролируемого изменения объекта через посторонние ссылки. На объект может одновременно существовать либо неограниченное число действительных «неизменяемых» ссылок (дающих доступ к объекту только на чтение), либо ровно одна «изменяемая» ссылка (дающая доступ на чтение и запись). Компилятор статически отслеживает время жизни объектов: любая операция, которая хотя бы потенциально может привести к сохранению ссылки на объект после выхода его владельца из области видимости, приводит к ошибке компиляции, что исключает появление «висячих ссылок» и утечек памяти. Такой подход усложняет технику программирования (соответственно, затрудняя обучение языку), но устраняет необходимость как ручного выделения и освобождения памяти, так и использования сборки мусора.
Сборка мусора как непременный атрибут среды исполнения программ используется в языках, основанных на декларативной парадигме, таких как LISP, ML, Пролог, Haskell. Её необходимость в этом случае обусловлена самим характером этих языков, не содержащих средств ручного управления временем жизни объектов и не имеющих возможности естественной интеграции подобных средств. Основной сложной структурой данных в таких языках обычно является динамический односвязный список, состоящий из динамически выделяемых списочных ячеек. Списки постоянно создаются, копируются, дублируются, компонуются и разделяются, что делает практически нереальным ручное управление временем жизни каждой выделенной списочной ячейки.
В императивных языках сборка мусора является одним из вариантов, наряду с ручным и некоторыми альтернативными методиками управления памятью. Здесь она рассматривается как средство упрощения программирования и предотвращения ошибок . Одним из первых компилируемых императивных языков со сборкой мусора стал Oberon, продемонстрировавший применимость и достаточно высокую эффективность этого механизма для данного типа языков, но широкую известность и популярность данному подходу принёс язык Java. Впоследствии подход Java был повторён в среде .NET и практически всех работающих в ней языках, начиная с C# и Visual Basic .NET. В то же время появилось множество интерпретируемых языков (JavaScript, Python, Ruby, Lua), куда сборка мусора включалась из соображений доступности языка для не-программистов и упрощения кодирования. Увеличение мощности аппаратуры, происходившее одновременно с совершенствованием самих сборщиков привело к тому, что дополнительные накладные расходы на сборку мусора перестали быть существенными. Большинство современных императивных языков со сборкой мусора вообще не имеют возможностей для явного ручного удаления объектов (например, оператора delete). В системах, использующих интерпретатор или компиляцию в байт-код, сборщик мусора является частью среды исполнения, в тех же языках, которые компилируются в объектный код процессора, он реализуется в виде обязательной системной библиотеки.
Имеется также небольшое количество языков (nim, Modula-3, D), поддерживающих как ручное, так и автоматическое управление памятью, для чего приложение использует две отдельные кучи.
Для улучшения этой статьи желательно: |
Данная страница на сайте WikiSort.ru содержит текст со страницы сайта "Википедия".
Если Вы хотите её отредактировать, то можете сделать это на странице редактирования в Википедии.
Если сделанные Вами правки не будут кем-нибудь удалены, то через несколько дней они появятся на сайте WikiSort.ru .