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