Сборщик мусора в .NET Framework. Ликбез
Введение
Разрабатывая программы или сайты на дотнете, никогда особо не задумываешься насчет сборки мусора за собой. Сборка мусора это освобождение памяти используемого объекта. Скажем, загрузили мы картинку в память, нарисовали линию, потом сохранили обратно в файл. Казалось бы, больше мы с этой картинкой делать ничего не будем. Однако ссылка (reference) на нее и данные остались еще в памяти и жрет память эта картинка довольно много места.
Попытаюсь разжевать тему как можно проще, ведь в программировании существует много способов решить одну задачу, не так ли?
Управляемый и неуправляемый код
Для тех, кто программирует на неуправляемом коде (C/C++) хорошо помнят баги утечек памяти, неправильные указатели или попытки прочитать другую область памяти. Для тех, кто этим никогда не занимался, стиль способствует себе отведение памяти под объект (alloc), работу с ним, и потом вручную возвращать память системе (free). Утечки памяти как раз образуются при ситуациях, где память не возвращается системе. Получается, память тратится зря на объект, который нам больше не нужен (мусор). Также бывали случаи, когда программист пытался достучаться до объекта, память которого была уже освобождена. Величайший этому пример это ошибка в виде «Инструкция по адресу '<адрес>' обратилась к памяти по адресу '<адрес>'. Память не может быть "read"».
Наступила эра Явы, так называемый Си плюсы без плюсов (на самом деле разницы больше и лучше не сравнивать такие вещи как управляемый и неуправляемый код). Ява дала программисту указателей и работы с памятью. На самом деле, в Яве нет указателей, а только ссылки на данные. Единственный метод доступа к данным объекта это по ссылке на объект (object reference). Самый смак такой методики это среда выполнения кода сама решает, как размещать память под объект и как разгребать весь мусор объектов. Так как до объекта можно достучаться только по его ссылке, при достижении количества ссылок до нуля, объект можно выкинуть и освободить память. И вот у нас появляется сборщик мусора.
Сборка мусора
А теперь о самом принципе работы сборщика мусора в дотнете. Разработчик сам не может очистить память под объект, это делает сборщик мусора (System.GC).
Рассмотрим ниже код.
public class Program {
// точка входа программы
static void Main(string[] args) {
PersonName();
}
static void PersonName() {
Person person = new Person();
person.FirstName = "Василий";
person.LastName = "Пупкин";
Console.WriteLine(person.FirstName);
}
}
class Person {
public string FirstName; // Имя
public string LastName; // Фамилия
}
Так как после выполнения метода PersonName, объект person нигде больше доступен не будет, его можно удалить. Удаляется он не сразу, а добавляется в регистр сборщика мусора. При следующем круге сбора мусора (поток), он удалится. Заметим также, что память отводится в момент ключевого слова «new».
Когда же выполняется сборка мусора? Каков интервал? Предугадать сборку мусора невозможно. Сборка мусора требует ресурсов и времени. Она может случиться при подходе порога памяти, количеству отведенных объектов, или бездействии кода (например, залоченный поток).
Сборка мусора может вообще никогда не случиться, если в системе достаточно памяти. Однако вызвать её все же можно методом System.GC.Collect().
Если так подумать, получается, программа работает довольно долго, рост объектов и отведенной памяти растет, рано или поздно память закончится. Но если заботиться об отведении памяти не нужно, и вроде все как бы уже сделано за нас, то в чем казус? Такое вполне может случиться, если не все ссылки на объект освобождены. Опять, ссылки на объект автоматически освобождаются при выходе выполнения из области видимости переменной, как например, после вызова PersonName().
Бывают ситуации когда программа выполняет какую-ту большую роль и может иметь доступ к нескольким тысяч объектов. Например, серверы, игры, графические приложения, и т.п. В данном случае необходимо освобождать объект самому. Самый простой способ сделать это, это присвоить null переменной.
person = null;
Это пример самой функции PersonName. В данном случае переменная может быть объявлена статичной и доступна многим методам программы. Если после присваивания нуля мы попытаемся достучаться до данных объекта, произойдет исключение NullReferenceException.
Куча
Все объекты, которые хранятся в памяти программы, хранятся в такой вот управляемой куче (managed heap). Среда выполнения в данном случае имеет полный контроль над объектами и может их освобождать, перемещать, и т.д. Стоит не забыть еще и об неуправляемых объектах. Неуправляемый код в дотнете это вызовы в WinAPI, COM, и прочие сторонние сборки. У неуправляемых объектов тоже есть своя куча. Однако среда эту кучу не трогает вовсе.
Есть одна важная разница между ООП в управляемой и неуправляемой среде. Знаете что такое деструктор? Для тех кто в танке, это специальная функция которая вызывается в последних моментах жизни объекта перед его удалением из памяти. Деструкторы обычно пользуются для освобождения дополнительных ресурсов объекта, например, подключений к базам данных, открытые файлы, и т.д. Не совсем хорошая идея полагаться на такое в дотнете. Пока сборщик мусора не проснется и не дойдет до вашего объекта, все эти подключения открыты, байтики висят, и народ ноет и бросает говном в вентилятор «что проги на дотнете жрут дохуя памяти».
Для разумного освобождения памяти существует интерфейс IDisposable. Вызывая метод Dispose(), по идее, все ресурсы объекта должны освободиться. Почему именно по идее? Разработчик должен сам вызвать этот метод, когда он считает объект ему больше не нужен, что его можно удалить. Еще одни грабли. Сама среда выполнения не имеет понятия что такое IDisposable, это просто стандартный интерфейс как и IHttpHandler к примеру. Особенно много классов наследующих от IDisposable хранятся в пространстве имен System.Data.
Теперь самые последние грабли. Имеем метод Dispose() и деструктор. Как контролировать кто есть кто и кто удалил объект?
На данный момент все же лучше делать уборку в методе Dispose(). В самом конце, вызывать GC.SuppressFinalize который дает знать среде не выполнять дальнейшую уборку, так как уже все убрано за него.
public class Program {
// точка входа программы
static void Main(string[] args) {
PersonName();
}
static void PersonName() {
Person person = new Person();
person.FirstName = "Василий";
person.LastName = "Пупкин";
Console.WriteLine(person.FirstName);
person = null;
}
}
class Person : IDisposable {
public string FirstName; // Имя
public string LastName; // Фамилия
private bool _disposed = false; // флаг удаления объекта
public void Dispose() {
if (!_disposed) {
// очистить все ссылки
FirstName = null;
LastName = null;
// дать знать фреймворку что все в принципе уже сделано
GC.SuppressFinalize(this);
_disposed = true;
}
}
// деструктор
~Person() {
Dispose();
}
}