Россия, Челябинск, Южно-Уральский Государственный Университет (НИУ) |
Управление памятью и сборка мусора
Висячие ссылки и мусор
Различные ошибочные сценарии, возникающие в процессе утилизации мусора, могут быть сведены к двум основным проблемам (для простоты изложения будем считать, что программа написана на языке, в котором допускается явное управление памятью):
- Предположим, что программист создает две различных переменных, указывающих на одну и ту же структуру данных, а затем уничтожает одну из переменных вместе с ее содержимым (т.е. уничтожение памяти вместе с утилизацией). После этого вторая переменная указывает на неопределенную область памяти. Такие переменные называются висячими ссылками . Приведем пример на С:
void* p = malloc (32000); q = p; free (p); // освобождает память, на которую указывает p, но указатель в q не // уничтожается и возникает висячая ссылка
- Для избежания первой проблемы можно предложить такую схему работы, в которой уничтожение памяти сводится только к разрушению пути доступа, а физически память не возвращается до тех пор, пока не будет уничтожена последняя переменная, использующая эту память. Однако тогда из-за трудности отслеживания всех путей доступа к данной структуре может возникнуть такая ситуация, когда все переменные будут уничтожены, а память так и не возвращена. В таком случае говорят, что память стала мусором.
Проиллюстрируем на еще одном примере:
void* p = malloc (32000); p = q; // уничтожает единственный указатель на память, делая ее мусором
Можно сказать, что висячие ссылки возникают в тех случаях, когда память утилизируется "слишком быстро" (т.е. раньше, чем память действительно перестает использоваться), а мусор - когда память утилизируется "слишком медленно" (т.е. позже, чем она могла бы быть возвращена). Висячие ссылки более опасны, так как могут приводить к некорректной работе программы, в то время как появление мусора вполне допустимо. Борьбу с мусором обычно возлагают на специальный процесс, называемый сборкой мусора (garbage collection) .
Статическая и динамическая память
При создании компилятора необходимо различать два важных класса информации о программе:
- Статическая информация, т.е. информация, известная во время компиляции
- Динамическая информация, т.е. сведения, неизвестные во время компиляции, но которые станут известны во время выполнения программы
Например, значения констант в строго типизированных языках известны уже во время компиляции, в то время как значения переменных в общем случае становятся известными уже только во время выполнения программы. Статически мы знаем количество веток в операторе switch, но определить, какая из них выполнится, мы сможем уже только во время выполнения программы.
В приложении к управлению памятью разделение всей информации на статическую и динамическую позволяет определить, каким механизмом распределения памяти необходимо пользоваться для той или переменной, структуры или процедуры. Например, размер памяти, необходимой под простые переменные, можно вычислить (и, соответственно, выделить необходимую память) уже во время компиляции, а вот память, запрашиваемую пользователем с размером, заданным с помощью переменной, придется выделять уже во время выполнения программы. Понятно, что статическое распределение памяти при прочих равных условиях предпочтительнее ("дешевле").
Особенно интересны "пограничные" случаи, такие, как выделение памяти под массивы. Дело в том, что размер памяти, необходимой под массивы фиксированного размера, в большинстве современных языках программирования можно посчитать статически. И тем не менее, иногда распределение памяти под массивы откладывают на этап выполнения программы. Это может быть осмысленно, например, для языков, разрешающих описание динамических массивов, т.е. массивов с границей, неизвестной во время компиляции. К таким языкам относятся Алгол 68, PL/I, C#. В этом случае механизм распределения памяти будет одинаковым для всех массивов. А вот в Паскале или С/С++ память под массивы всегда можно выделять статически.
Влияние управления памятью на языки программирования
При выборе того или иного метода распределения памяти необходимо учитывать специфику исходного языка программирования, так как практически все языки программирования ориентированы на какой-то механизм управления памяти, который должен быть основным (или даже единственным) для данного языка.
Например, язык Фортран изначально проектировался таким образом, чтобы для программ на Фортране достаточно было использовать только статическое распределения памяти и, следовательно, написание компиляторов Фортрана упрощалось. Для этого пришлось пожертвовать многими языковыми возможностями, которые сегодня считаются общепринятыми. Например, синтаксис Фортрана легко было бы расширить для включения в него рекурсивных процедур, но тогда при компиляции программ пришлось бы использовать стековый механизм распределения программ.
В языке С подразумевается использование более широкого набора методов управления памятью, но при этом ответственность за управление памятью в большинстве случаев возлагается на самого программиста. Таким образом, достигается некоторое равновесие между создателем компилятора и конечным программистом: сложность написания компилятора несколько увеличивается, но при этом язык предоставляет большие возможности. Программист же получает в свои руки мощный механизм управления памятью. Если программист с ним не справится - тем хуже для программиста.
Наконец, языки Java и C# не предоставляют программисту никакого механизма для явного выделения или освобождения памяти. В этих языках вся ответственность за управление памятью лежит на механизме сборки мусора, а следовательно, на разработчиках компилятора.