Утечки памяти в программах на Perl
Статья была опубликована в 5.2004 номере журнала "Системный администратор".
Perl прекрасный язык (и мы сегодня в этом ещё убедимся), позволяющий
программисту весьма кратко выразитьдостаточно ёмкие мысли. Приведу
пример программы, состоящей из одной строки и демонстрирующей утечку
памяти в Perl:
# Осторожно утечка памяти
while (1) { my ($a,$b); $a=\$b; $b=\$a }
Если вы решите испытать эту нехитрую программу, то приготовьтесь к тому,
что события будут развиваться стремительно. На моей станции она съедает
всю память за доли минуты.
В ней, как вы видите, реализован бесконечный цикл. В теле цикла созданы
две локальные переменные $a и $b. Ожидается, что, как и положено
локальным переменным, они будут уничтожаться каждый раз по окончании
выполнения тела цикла, но этого не происходит. Почему?
Причина кроется в устройстве системы сборки мусора. Последняя просто
ведёт учёт всех ссылок, сделанных на переменную. Если переменная вышла
из области видимости, но ссылки на неё остались, то данные не удаляются
(хотя имя переменной становится недоступным). Это обеспечивает
работоспособность всех ссылок, но может ввести в заблуждение систему
сборки мусора. Обратите внимание, ссылки не анализируются на предмет,
будут они удалены в ближайшее время или не будут. Они подсчитываются все
без исключения.
Иллюстрацией к сказанному может послужить абсолютно бытовая ситуация,
встречающаяся сплошь и рядом:
my $ref;
{ my $var=1975; $ref=\$var; }
# print $var # неверно, имя $var уже не доступно
print $$ref;
# будет напечатано '1975', данные не исчезли
Как видите, за пределами блока имя $var уже не доступно, но данные,
ранее ассоциированные с этим именем, сохранились и будут в сохранности,
пока не будет удалена последняя ссылка на них [1].
В нашем однострочном примере переменные $a и $b содержат ссылки друг на
друга. Несмотря на то, что их имена становятся не видны сразу по
окончании выполнения блока, система сборки мусора не удаляет данные,
ассоциированные с ними, неуклонно следуя своему алгоритму: данные
считаются мусором только в том случае, если ссылок на них не осталось.
Может быть, кому-то покажется, что подобная ситуация попахивает
надуманностью и встречается редко?
Отнюдь. Например, у вас может быть массив структур [2], описывающих товары,
и другой массив структур, описывающих магазины. Структуры, описывающие
товары, могут содержать указатели на магазины, а структуры, описывающие
магазины, могут ссылаться на элементы списка товаров. Вот мы и получили
две конструкции, ссылающиеся друг на друга. Больше того, реляционные
базы данных просто-таки сами подсказывают подобные решения.
Давайте разберёмся, как же решить проблему утилизации ненужных данных.
Наименее громоздким мне представляется следующий объект для исследований:
01: #!/usr/bin/perl -w
02:
03: # Осторожно ! Утечка памяти !
04:
05: sub create_ring {
06: my ($length)=@_;
07: my @ring=({'value' => 0})x$length;
08: for (my $i=0; $i<$length; $i++) {
09: $ring[$i]{'next_ref'}=$ring[($i+1)%$length];
10: }
11: return @ring;
12: }
13:
14:
Процедура create_ring создаёт и возвращает массив хэшей, каждый из
которых имеет два ключа. Под именем value хранится некая величина, под
именем "next_ref" ссылка на следующий элемент массива. Последний
элемент несёт ссылку на первый элемент (номер ноль), зацикливая
конструкцию.
Вызывая её в бесконечном цикле и следя за процессами в системе, легко
убедиться, что в приведённой программе есть утечка памяти. Элементы
списка, создаваемого create_ring, не удаляются по той же причине, что и
$a и $b из первого примера: когда выполнение блока доходит до конца,
остаются неудалённые ссылки на элементы списка (как вы помните, эти
ссылки содержатся в хэшах, составляющих список, под именами "next_ref").
Ситуация не изменится даже в том случае, если чуть изменить наш код
(привожу только важные и изменён ные строки):
...
03: # Осторожно ! Утечка памяти !
...
11: return $ring[0];
...
14: while (1) { my $ring=create_ring(10) }
Не поможет и такое изменение:
...
03: # Осторожно ! Утечка памяти !
...
11: return \@ring;
...
14: while (1) { my $ring=create_ring(10) }
В двух последних примерах по окончании выполнения блока мы будем терять
ссылку на элемент массива (в первом) или ссылку на сам массив (во
втором), но данные массива, уже никому не доступные, будут бережно
сохраняться системой сборки мусора.
Один из способов освободить память разорвать этот круг (вернее, ring)
вручную, добавив в конец блока соответствующую инструкцию:
...
14: while (1) {
15: my $ring=create_ring(10);
16: $ring->[0]{'next_ref'}=undef;
17: }
Одного разрыва вполне достаточно, чтобы система сборки мусора один за
другим удалила все элементы структуры, но этот способ не выглядит
изящно. Мы просто взяли на себя труд убрать мусор, но так и не заставили
работать над этим интерпретатор Perl. Неужели всё так безнадёжно? Нет! В
Perl есть механизмы, позволяющие растолковать ему все детали нашего
замысла, я говорю об объектно-ориентированном подходе.
Первый пример будет основан на связывании переменной с классом [3].
Я буду рассчитывать на то, что читатель знает об устройстве модулей Perl
и знаком с механизмом связывания объектов с переменными. Вкратце скажу,
что Perl позволяет [4] ассоциировать переменную с классом с помощью
оператора tie. После такого связывания переменная начинает жить по
собственным законам, при любой манипуляции с ней вызываются соответствующие
методы объекта, с которым она связана.
Вот код, реализующий обозначенный подход:
01: #!/usr/bin/perl -w
02:
03: package MyRing;
04:
05: sub create_ring {
06: my ($length)=@_;
07: my @ring=({'value' => 0})x$length;
08: for (my $i=0; $i<$length; $i++) {
09: $ring[$i]{'next_ref'}=$ring[($i+1)%$length];
10: }
11: return \@ring;
12: }
13:
14: sub TIESCALAR {
15: my ($class, $length)=@_;
16: return bless create_ring($length), $class;
17: }
18:
19: sub FETCH { return $_[0]; }
20:
21: sub STORE { die "писать в MyRing нельзя.\n" }
22:
23: sub DESTROY {
24: my ($self)=shift;
25: $self->[0]{'next_ref'}=undef;
26: }
27:
28: package main;
29:
30: # демонстрация работоспособности
31: {
32: my $a;
33: {
34: tie my $ring, 'MyRing', 10;
35: $ring->[1]{'value'}='test';
36: $a=$ring;
37: }
38: # $ring больше не видна, но данные целы
39: print $a->[0]{'next_ref'}{'value'}."\n";
40: # здесь вызывается DESTROY
41: }
42: # $ring=7; # это вызовает метод STORE
43:
44: while (1) { tie my $ring, 'MyRing', 10; }
Для упрощения я не стал выделять модуль, описывающий класс, в отдельный
файл. Связывать объект будем со скалярной переменной, это тоже,
наверное, не лучшее решение: естественнее было бы выбрать массив, а если
бы мы захотели сделать нашу разработку более масштабируемой и
развиваемой, то логичнее было бы выбрать хэш. Но для связывания таких
сложных конструкций нам пришлось бы реализовать множество методов, для
скаляра же достаточно четырёх, эта компактность и наглядность лучше
всего подойдёт для обсуждения в статье.
Итак, в строках 3-27 описан модуль MyRing. Он со держит уже знакомую нам
функцию создания и инициализации массива хэшей (create_ring) и методы,
необходимые для обеспечения работоспособности связанной переменной.
Метод TIESCALAR (строка 14) вызывается в момент связывания, он получает
от оператора tie список дополнительных параметров, в котором у нас будет
только одна величина длина требуемого массива. Получив параметры, метод
создаёт наш массив (вызов create_ring($length)), ассоциирует его с
классом (вызов bless) и возвращает новоиспечённый объект.
Метод FETCH (строка 19) вызывается, когда выполняется чтение значения
связанной переменной. Ему передаётся один параметр сам объект. Наша
реализация FETCH не делает ничего, просто возвращает то, что получила
без изменений.
Метод STORE (строка 21) отвечает за запись в переменную. Я не придумал,
что в него написать, он просто выдаёт грозное сообщение и убивает
программу.
И наконец, метод DESTROY (строка 23) это то, что нам нужно. Он
вызывается автоматически всегда, когда переменная выходит за область
видимости или становится недоступна по другим причинам (например, когда
программа завершается). Наш метод DESTROY разрывает кольцевую структуру
(строка 25 выглядит знакомо, не правда ли?), позволяя системе сборки
мусора довести свою работу до конца.
Со строки 28 начинается основная программа, где мы сейчас и
воспользуемся нашим классом MyRing.
В строках 30-42 приведён небольшой фрагмент кода, демонстрирующий
работоспособность нашей кухни.
В строке 32 мы создаём локальную переменную $a, которая будет
существовать только в пределах блока, находящегося в строках 31-41. Во
вложенном блоке (строки 33-37) мы создаём локальную переменную $ring,
которую сразу же связываем с классом MyRing (строка 35).
Для проверки корректности структуры $ring записываем строку test в её
первый узел (строка 36). Для проверки корректности сборки мусора
сохраняем копию $ring в $a. Что произойдёт, когда вложенный блок
закончится?
Оказывается, метод DESTROY не будет вызван! Это и понятно, мы сохранили
указатель на нашу структуру (помните? $ring является указателем на
массив) в переменной $a, а она всё ещё существует. Переменной $ring не
стало, но пропало только имя, данные целы. Мы убеждаемся в этом в строке
39. Здесь же мы убеждаемся в корректности структуры $ring (теперь $a),
получив доступ к первому узлу как к узлу, следующему после нулевого. А
метод DESTROY будет вызван только тогда, когда исчезнет переменная $a.
Таким образом, все работает правильно.
Обратите внимание и на то, что при выполнении манипуляций с $ring
(строка 35) вызывается метод FETCH, отвечающий за чтение переменной, а
не STORE, ответственный за запись. Действительно, чтобы проделать
операцию, описанную в строке 35, нам пришлось считать значение указателя
$ring, а не записать его. А вот строка 42 не случайно закомментирована.
Операция присвоения автоматически вызывала бы метод STORE, а он бы
аварийно остановил выполнение программы.
Наконец в строке 44 реализован всё тот же бесконечный цикл. На каждом
проходе в его теле вновь создаётся локальная переменная $ring, но теперь
мы связываем её с классом MyRing. Благодаря этому по завершении блока
Perl передаёт управление методу DESTROY, который корректно освобождает
память.
Таким образом, нам больше не приходится удалять кольцевую структуру
руками. Мы научили систему сборки мусора обращаться с нашими данными, и
Perl теперь сам (...почти) защищает нас от утечек памяти.
Я продемонстрировал предельно мудрёную схему, можно ли обойтись меньшей
кровью? Конечно! В Perl грань объектно-ориентированного программирования
весьма размыта. Взгляните на следующий код, в нём нет почти ничего,
напоминающего о его объектно-ориентированности:
01: #!/usr/bin/perl -w
02:
03: package MyRing;
04:
05: sub main::create_ring {
06: my ($length)=@_;
07: my @ring=({'value' => 0})x$length;
08: for (my $i=0; $i<$length; $i++) {
09: $ring[$i]{'next_ref'}=$ring[($i+1)%$length];
10: }
11: return bless \@ring, __PACKAGE__;
12: }
13:
14: sub DESTROY {
15: my ($self)=@_;
16: $self->[0]{'next_ref'}=undef;
17: }
18:
19: package main;
20:
21: while (1) { my $ring=create_ring(10); }
Обратите внимание, вызов create_ring в основной программе вообще ни чем
не выдаёт объектно-ориентированную природу переменной $ring. Правда,
саму create_ring нам пришлось чуть доработать, сделав её каким-никаким,
а всё-таки конструктором (вызов bless в строке 11), и экспортировав5 её
в модуль main (имя main::create_ring в строке 5).
Метод DESTROY остался без изменений. Вызываться он будет в тех же
случаях и обеспечит такие же функции, как и в предыдущем примере.
Я бы не стал говорить, что какой-то из двух приведённых методов решения
проблемы лучше, а другой хуже. У каждого есть свои преимущества и
недостатки. Первый длинноват, но код последователен и легко читается.
Второй компактен, но менее универсален и его логику понять сложнее
(вернее, сложнее разглядеть в нём объектно-ориентированный подход).
Первый код работает чуть медленнее, поскольку переменная связана. Второй
работает чуть быстрее, но допускает выполнение бессмысленных действий,
например:
$ring=7;
О плюсах и минусах можно спорить бесконечно. Кроме того, существует
великое множество промежуточных подходов, сочетающих традиционное и
объектноориентированное программирование в разных пропорциях. Приводить
и обсуждать их все я, конечно, не буду, пусть каждый пишет код так, как
привык.
Гораздо интереснее задать другой вопрос: насколько надёжна наша защита
от утечек памяти? Не будем наивны, она тоже небезупречна и болеет теми
же бо лезнями, что и однострочная программа из самого первого листинга.
Не будем далеко ходить за примером и чуть модифицируем тело цикла из
последнего листинга:
...
21: # Оссторожно ! Утечка памяти !
22: while (1) {
23: my $ring=create_ring(10);
24: my $a;
25: $ring->[0]{'value'}=\$a;
26: $a=$ring;
27: }
Утечка произошла по той же самой причине, что и обычно. Когда выполнение
блока подходит к концу и на ступает время собрать мусор, система сборки
обнару живает, что на кольцевую структуру ссылаются две переменных:
$ring и $a. Вторую уничтожить не получается, потому что на неё имеется
ссылка. Где эта ссылка находится, система сборки мусора уже не
разбирается, но мы-то знаем, что она содержится в недрах структуры
$ring. Круг замкнулся, система сборки мусора снова не заметила наш
мусор.
Теперь, я надеюсь, читатель видит и причины возникновения проблемы, и
пути её решения. Одним словом, при создании сложных структур данных
всегда надо помнить, что Perl не сможет угадать ваши мысли, и чётко
представлять, каким образом он интерпретирует ваши команды.
Уточнения:
1
Во многих языках, например в Cи, ситуация обрат-
на: данные могут быть удалены автоматически, даже
если на них имеются указатели. Perl же не требует
повышенной аккуратности при работе с указателя-
ми, всегда поддерживая их работоспособными.
2
Конечно, это жаргон, здесь и далее фразы типа
массив структур следует понимать как массив
указателей на структуры.
3
В Perl понятия класс, модуль и пакет так же
не разделимы, как и понятия объект и перемен-
ная, метод и функция. Поэтому я не буду при-
держиваться строгой объектно-ориентированной
терминологии, в Perl её просто нет.
4
Начиная с версии 5.0.
5
Конечно, это только имитация настоящего экспор-
тирования. Подобный подход не позволит нормаль-
но использовать модуль MyRing в других модулях,
но я не хотел бы здесь уделять чрезмерное внима-
ние созданию модулей в Perl, и мой код демонстри-
рует не хорошие манеры при написании модулей, а
предельно компактное решение.