![]() |
![]() |
|
||
![]() |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [8 июля 2002 г.] |
Любая программа в своей работе использует данные, хранящиеся в памяти. Эта память не статична, в том смысле, что постоянно возникает необходимость выделять в ней все новые и новые неперекрывающиеся участки, чтобы разместить там некоторые строки, числа, массивы и т. д. Одно из
Конечно, эти 4 ГБ виртуальны, и часть из них может находиться в файле подкачки. Но важно то, что для приложения все это незаметно, поэтому оно «думает», что работает одно на машине и со всей доступной на ней памятью. Обеспечивает такую «прозрачность» операционная система и архитектура процессора. |
Однако, если приложение динамично, рано или поздно память закончится. Возникает необходимость высвобождать участки, которые больше не требуются, тем самым делая их доступными программе в дальнейшем. Такие участки называют мусором, а их
В этой набле я расскажу о трех способах сборки мусора, применяемых в программах, а также об их главных достоинствах и недостатках.
В языках сравнительно низкого уровня, таких как C++ или Delphi, обычно применяется ручная сборка мусора. Иными словами, выделив некоторый участок памяти и сохранив его адрес в переменную, программист сам должен позаботиться о его высвобождении в дальнейшем:
| Листинг 1 |
// выделяем блок памяти длиной 1024 байта char *p = new char[1024]; ... // работаем с ним ... // освобождаем эту память delete p; |

Однако здесь есть три момента. Во-первых, если операторов выделения памяти много, то вполне можно забыть освободить какой-то участок, что рано или поздно приведет к нехватке памяти или обычному «раздуванию» процесса.
Во-вторых, если код, который обозначен в листинге многоточием, находится в процедуре и внезапно возбудит исключение или выполнит обыкновенную инструкцию
Наконец, в-третьих, программы редко когда бывают линейными, и особенно это относится к объектно-ориентированному программированию. Выделив когда-то память под объект, мы не знаем точно, в какой момент времени ее можно будет освободить. Особенно это характерно для ситуаций, когда один и тот же объект A используется несколькими другими объектами X, Y, Z (последние, например, содержат указатели на первый). Предположим, что X уже не нуждается в A, но мы не можем тут же его удалить, ведь, возможно, A все еще используется объектами Y и Z. Иными словами, память (и объект) A нельзя удалять, пока на него кто-то ссылается. Ручное отслеживание таких ситуаций является обычной рутиной, а значит, потенциально приводит к ошибкам.
Таким образом, видно, что ручная сборка мусора (какой бы она ни была совершенной) в крупных программах оказывается непригодной, а значит, язык программирования должен поддерживать автоматическую сборку мусора, руководствуясь правилом: память можно освободить, когда в программе на нее больше нет ссылок.
По-видимому, задача создания идеального автоматического сборщика мусора алгоритмически неразрешима (скоро вы поймете, что я имел в виду). Тем не менее, кое-что предложить все-таки можно. Давайте рассмотрим два наиболее популярных алгоритма на примерах языков Perl и Java.
|
В Perl не существует указателей, как в C++, но зато есть такое понятие, как ссылка. О ссылках я писал в предыдущих наблах, так что рекомендую ознакомиться. Объекты, на которые в программе не осталось ссылок, Perl немедленно удаляет из памяти. Вся специфика заключена в словах «не осталось ссылок» и «немедленно».
Вернемся к «гардеробной теории» из двадцать первой наблы, где в качестве объекта выступает пальто, а в качестве |
Сложности начинаются, когда на некоторый объект имеется более одной ссылки. В этом случае, конечно же, уничтожение нужно провести только при обнулении последней ссылки, но ни в коем
В этом и заключена специфика алгоритма со счетчиком ссылок, одновременно его сила и слабость. Каждый объект (вернее блок памяти) содержит в себе скрытое поле, хранящее так называемый счетчик ссылок. Каждый раз, когда в программе появляется новая ссылка на объект, этот счетчик увеличивается на 1 (обычно это происходит при выполнении операции присваивания
В рамках «гардеробной теории» все работает следующим образом. |
Удаление
Казалось бы, все замечательно, но дело в том, что алгоритм Perl имеет один очень существенный недостаток, который сильно ограничивает его применимость. Речь идет о циклических ссылках. Давайте рассмотрим такой код:
| Листинг 2 |
# ссылка на структуру-дерево
my $tree;
{
my (%parent,%child);
# отец ссылается на сына
$parent{children}[0] = \%child;
# сын ссылается на отца
$child{parent} = \%parent;
# сохраняем ссылку на отца - ссылку на дерево
$tree = \%parent;
# (*)
} |
Здесь
| Листинг 3 |
# отсоединяем ссылку от %parent $tree = undef; # теперь к %parent никак нельзя обратиться! |
А
Все дело в злополучных счетчиках ссылок. Смотрите:
Вы сдаете в гардероб свое пальто и чужое (которое взяли только что по поддельному номерку). При этом для конспирации номерок от своего пальто кладете в карман чужого, а от |
Теперь нетрудно сделать общий вывод: алгоритм попросту не срабатывает, когда в программе имеются кольцевые ссылки (первый объект ссылается на второй, а
| Листинг 4 |
{
my $a;
$a = \$a; # ссылка сама на себя!
} |
Чем не замкнутая внутри себя вселенная?.. Мы получили лексическую переменную, которая, несмотря на окончание блока, все равно продолжает существовать в памяти, занимая место, но не будучи доступна.
Напоминаю, что лексическая переменная далеко не обязательно удаляется при выходе из блока. Можно сказать, что выход из блока для лексической переменной равносильно уменьшению счетчика ссылок на 1, не более того. Так что инструкция |
Можно ли каким-либо способом обойти эту проблему?.. Очевидно, нет: даже если интерпретатор и заподозрит, что пара
Например, он уничтожит вначале первый,а |
Так что единственный
| Листинг 5 |
# удаляем кольцевые ссылки
delete $tree->{children};
# отсоединяем ссылку от %parent
$tree = undef; |
Излишне даже заикаться, что этот способ «ручной», а значит, потенциально подвержен ошибкам.
Но что же все-таки происходит с «повисшими» в памяти объектами, на которые больше никто не ссылается?.. Перед окончанием работы программы они принудительно удаляются в произвольном (!) порядке. До сих пор все объекты были простыми хэшами, не содержащими деструкторов, поэтому такой вариант для них вполне подходил. Ситуация меняется при использовании ООП и деструкторов. «Произвольность» чистки мусора в этом случае дает о себя знать сообщением вроде «error during global destruction», что, например, происходит при попытке в деструкторе обратиться к объекту, уже не существующему (что невозможно в обычной программе). |
Вывод: Perl полностью поддерживает деструкторы, однако его сборщик мусора работает не ахти как при наличии кольцевых ссылок в программе.
Можно ли решить проблему «повисших» объектов?.. Оказывается, можно, но решение это не дастся легко, а потребует серьезных жертв. Эти
Нередко эти жертвы оказываются чрезмерными (особенно отказ от деструкторов). Тем не менее, правильная работа с кольцевыми ссылками в некоторых классах приложений (особенно GUI, очень |
Чтобы понять, в чем же дело, рассмотрим вкратце, как работает «обходной» алгоритм сборки мусора на примере «гардеробной теории».
В связи с сильно возросшей смертностью администрация выпустила новое правило, согласно которому гардеробщик должен раз в день проверять, жив ли еще владелец того или иного пальто. Если нет, одежду нужно снять и пожертвовать на благотворительные цели. Для этого каждый вечер гардеробщик начинает обзванивать всех владельцев одного за другим (на это время новые посетители не обслуживаются во избежание путаницы). Если владелец отвечает, работник наклеивает бирку на крючок его пальто. Обзвонив всех, гардеробщик сдает в утиль каждое пальто, не имеющее бирок.
|
Сборка мусора в программе происходит следующим образом. Периодически (например, раз в секунду) программа приостанавливается и начинает выполняться сборщик мусора. Первым делом он обходит все объекты, существующие в программе, и обозначает их как необработанные (для этого, как и в алгоритме подсчетка ссылок, у объекта есть специальное поле пометки). Затем сборщик мусора берет все переменные, доступные в текущем контексте (где только что остановилась программа), и помечает каждый объект, на который они ссылаются, как обработанный. Но ведь объекты тоже включают в себя поля, являющиеся ссылками! Поэтому, найдя очередной объект, сборщик мусора извлекает из него все ссылки и также проходится по ним рекурсивно. При этом, если он наткнется на уже помеченный объект, он его пропускает. Зачем делать дважды одну и ту же работу? Неудивительно, что в конце всего этого длинного обхода непомеченными оказываются только те объекты, на которые в программе больше нет ссылок. Остается только их удалить, что и делается (в произвольном порядке, как во время финальной «чистки» в Perl). Обратите внимание, что кольцевые ссылки алгоритмом отслеживаются без каких-либо проблем.
Осталось еще добавить, что гардеробщик знает о проделках некоторых своих клиентов класть чужие номерки в карман пальто, которые потом ему сдаются. Поэтому после обзвона всех владельцев он также обшаривает карманы висящей одежды и обрабатывает все номерки, которые там находятся. А заодно и получает чаевые, если повезет.
![]() |
Рассмотрим подробнее недостатки алгоритма. Первый связан с быстродействием. Ведь если в программе существуют миллионы маленьких объектов, сборщику мусора придется каждый раз обходить их все и, фактически, проделывать большинство работы впустую.
Говорят, что алгоритм со счетчиком ссылок дает небольшую, но равномерную нагрузку на процессор, в то время как алгоритм |
Второй недостаток связан с отложенностью освобождения памяти. Ведь объект может еще довольно долго существовать в памяти, хотя он уже перестал использоваться. Таким образом, деструктор объекта вызывается не сразу же после того, как на были потеряны все ссылки, а некоторое время спустя. В большинстве случаев это неприемлемо, поэтому можно сказать, что деструкторов в алгоритме обхода не бывает.
Тем не менее, практически все языки позволяют создавать функцию, которая будет вызвана при фактическом освобождении памяти. Ее обычно называют финализатором, чтобы не путать с деструктором (ибо это несколько разные вещи). Финализаторы применяются очень редко (вы, наверное, не раз прочтете об этом в любой книге по Java или C#), а реальное разрушение объекта (вызов деструктора) возлагается на программиста. То есть, последний должен написать свою функцию, выполняющую роль деструктора, и вызвать ее, когда в объекте отпадает необходимость. Излишне говорить, что это «лобовое» решение недалеко ушло от «ручного» способа сборки мусора, а потому крайне неудобно (возникают те же самые проблемы с совместным использованием объекта, что и прежде).
Конечно, изложенный алгоритм обхода обычно реализуется со значительными оптимизациями, которые я здесь опускаю из соображений бессмысленности. |
Вывод: единственное достоинство алгоритма
Итак, какой же из последних двух алгоритмов лучше?..
К сожалению, в общем случае на этот вопрос ответа нет. Оба алгоритма обладают взаимоисключающими достоинствами и недостатками, поэтому там, где хорош один, плох другой, и наоборот.
Например, если в программе требуется активно работать со связанными списками и гипердеревьями (когда некоторые вершины могут ссылаться на своих предков), выгоднее будет применять Java и алгоритм сборки мусора с обходом (потому что он умеет разрывать кольцевые ссылки). Как показывает практика, GUI-приложения почти всегда связаны с необходимостью создания кольцевых ссылок (Java и C# весьма популярны в GUI-программировании).
Если же требуется работа с линейными или ветвящимися структурами (обычными деревьями), но критично наличие деструкторов, следует предпочесть алгоритм, основанный на счетчиках ссылок. Web-программирование является хорошим примером области, удовлетворяющей этому требованию (в частности, механизм работы модуля CGI::WebOut существенно основан на деструкторах).
Кстати говоря, никто не утверждал о невозможности объединения двух алгоритмов в один. При этом, если кольцевых ссылок в программе нет, будет нормально работать алгоритм на счетчиках, а если
![]() |
| ||||||||||||||||||||||||||||
| Дмитрий Котеров | 8 июля 2002 г. ©1999-2010 | | Контакт | Вернуться к оглавлению |