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

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

Автоматическая сборка мусора позволяет повысить безопасность доступа к памяти.

История

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

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

Ручное управление памятью

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

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

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

Висячие ссылки

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

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

Утечки памяти

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

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

Механизм сборки мусора

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

Основные принципы

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

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

Достижимость объекта

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие механизмы

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

Требования к языку и системе

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

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

Проблемы использования

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

Освобождение других ресурсов, занятых объектом
Помимо динамической памяти, объект может владеть и другими ресурсами — подчас более ценными, чем память. Если объект при создании открывает файл, по завершении использования он должен его закрыть, если подключается к СУБД — должен отключиться. В системах с ручным управлением памятью это делается непосредственно перед удалением объекта из памяти, чаще всего — в деструкторах соответствующих объектов. В системах со сборкой мусора обычно есть возможность выполнить некоторый код непосредственно перед удалением объекта, так называемые финализаторы, но для освобождения ресурсов они не годятся, так как момент удаления заранее неизвестен, и может оказаться, что ресурс освобождается намного позже, чем перестаёт использоваться объект. В подобных случаях программисту всё равно приходится отслеживать использование объекта вручную и вручную выполнять операции по освобождению занятых объектом ресурсов. В C# специально для этой цели существует интерфейс IDisposable, в Java — AutoCloseable.
Утечка памяти
В системах со сборкой мусора тоже могут возникать утечки памяти, правда, имеющие несколько другую природу. Ссылка на неиспользуемый объект может сохраниться в другом объекте, который используется и становится своеобразным «якорем», удерживающим ненужный объект в памяти. Например, созданный объект добавляется в коллекцию, используемую для вспомогательных операций, потом перестаёт использоваться, но не удаляется из коллекции. Коллекция удерживает ссылку, объект остаётся достижимым и не подвергается сборке мусора. Результатом становится всё та же утечка памяти.
Чтобы устранить подобные проблемы, среда исполнения может поддерживать специальное средство — так называемые слабые ссылки. Слабые ссылки не удерживают объект и превращаются в null, как только объект исчезает — поэтому код должен быть готов к тому, что однажды ссылка укажет в никуда.
Потеря эффективности при операциях с частым выделением и освобождением памяти
Некоторые действия, вполне безобидные для систем с ручным управлением памятью, в системах со сборкой мусора могут порождать несоразмерно большие накладные расходы. Классический пример подобной проблемы приведён ниже.
    String out = "";
    // Предполагается, что strings содержит большое количество коротких строк,
    // из которых нужно собрать одну большую строку в переменной out. 
    for( String str : strings ) {
        out += str; // Данный код будет каждую итерацию создавать 
                    // новую строковую переменную и выделять под неё память.
    }
Данный код на языке Java выглядит так, как будто переменная out, созданная однажды, в цикле каждый раз «дописывается» новой строкой. В действительности же строки в Java неизменяемы, поэтому в данном коде на каждом проходе цикла будет происходить:
  1. Создание новой строковой переменной достаточной длины.
  2. Копирование в новую переменную старого содержимого out.
  3. Копирование в новую переменную содержимого str.
  4. Присваивание переменной out ссылки на новую строковую переменную.
При этом каждый раз блок памяти, в котором ранее находилось значение переменной out, будет выходить из употребления и ждать до запуска сборщика мусора. Если объединяется подобным образом 100 строк по 100 символов, то суммарно на данную операцию будет выделено более 500000 байт памяти, то есть в 50 раз больше, чем размер конечной «длинной» строки.
Подобные операции, когда достаточно большие объекты в памяти часто создаются и тут же перестают использоваться, ведут к очень быстрому непродуктивному заполнению всей доступной памяти и частому запуску сборщика мусора, что в определённых условиях может сильно замедлить работу программы или, по крайней мере, потребовать выделения ей для работы неадекватно большого объёма памяти. Во избежание подобных проблем программист должен хорошо представлять себе механизм автоматического управления памятью. Также иногда могут использоваться специальные средства для эффективного выполнения опасных операций. Так, для оптимизации приведённого выше примера необходимо воспользоваться специальным классом StringBuilder, позволяющим одним действием выделить память сразу под всю строку, а в цикле только дописывать в конец этой строки очередной фрагмент.
Проблемы взаимодействия с инородным кодом и прямой работы с физической памятью
В практическом программировании на языках со сборкой мусора почти невозможно обойтись без взаимодействия с так называемым инородным кодом: API операционной системы, драйверы устройств, внешние программные модули, написанные на других языках, не управляются сборщиком мусора. Иногда возникает необходимость работы непосредственно с физической памятью компьютера; система управления памятью это также ограничивает, если вообще допускает.
Взаимодействие с инородным кодом обеспечивается одним из двух способов: либо на низкоуровневом языке (обычно на Си) пишется обёртка для инородного кода, скрывающая низкоуровневые детали, либо непосредственно в язык добавляется синтаксис, обеспечивающий возможность написания «небезопасного» (unsafe) кода — отдельных фрагментов или модулей, для которых программисту предоставлен более полный контроль за всеми аспектами управления памятью.
И первое, и второе решение имеет свои недостатки. Обёртки, как правило, сложны, требуют высокой квалификации для разработки, могут быть плохо переносимы. (Впрочем, их создание может быть автоматизировано. Например, существует мультиязыковой генератор SWIG, который по имеющимся заголовочным файлам Си/C++ автоматически создаёт обёртки для целого ряда языков, поддерживающих сборку мусора.) Они подвержены устареванию: обёртка, написанная для одной реализации языка, может стать непригодной для использования в другой, например, при переходе от неперемещающего сборщика мусора к перемещающему.
Специальный синтаксис для небезопасного кода является «легальной дырой» в механизме управления памятью и источником трудно обнаруживаемых ошибок; при этом самим своим наличием он провоцирует программиста на обход языковых ограничений.
Кроме того, любое вмешательство в работу сборщика мусора (а оно неизбежно при взаимодействии с инородным кодом) потенциально снижает эффективность его работы. Например, фиксация в памяти определённого региона, которая необходима, чтобы во время работы с этой памятью инородного кода сборщик мусора не удалил и не переместил его, может ограничить возможность дефрагментации памяти и тем самым затруднить последующее выделение фрагментов нужного размера, даже при наличии достаточного общего объёма свободной памяти.

Достоинства и недостатки

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

Считается, что сборка мусора заметно сокращает трудозатраты на управление памятью по сравнению с языками, где она не реализована. Согласно исследованию[3], программисты на Си тратят 30 % — 40 % общего времени разработки (не считая отладки) только на управление памятью. Впрочем, существуют исследования с противоположными выводами, например, в работе[4] утверждается, что реальная разница в скорости разработки программного обеспечения на C++, где нет автоматической сборки мусора, и на Java, где она реализована, невелика.

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

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

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

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

Альтернативы

Поддержка в некоторых императивных языках автоматического вызова деструктора при выходе объекта из области синтаксической видимости (C++[5], Ада, 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), поддерживающих как ручное, так и автоматическое управление памятью, для чего приложение использует две отдельные кучи.

Примечания

  1. Устоявшийся термин, с точки зрения русского языка правильнее «сбор мусора» (выдержка из словарей ABBYY Lingvo, словарь Ушакова: сборка, сбор, собрать; Gramota.ru: обсуждение). По словарю, сборка — это «соединяя отдельные части, детали, сделать, создать что-нибудь, превратить во что-нибудь готовое» и к остальным значениям слова «собрать» применяется именно «сбор».
  2. Реймонд Чен. Наверняка вы думаете о сборке мусора неправильно
  3. Boehm H. Advantages and Disadvantages of Conservative Garbage Collection. Архивировано 24 июля 2013 года.
    (ссылка изРеймонд, Эрик. Искусство программирования для Unix.. — 2005. — С. 357. — 544 с. — ISBN 5-8459-0791-8.)
  4. Lutz Prechelt. An empirical comparison of C, C++, Java, Perl, Python, Rexx, and Tcl (англ.). Технологический институт Карлсруэ. Дата обращения: 26 октября 2013.
  5. RAII, Dynamic Objects, and Factories in C++, Roland Pibinger, 3 May 2005
This article is issued from Wikipedia. The text is licensed under Creative Commons - Attribution - Sharealike. Additional terms may apply for the media files.