Типы значений. Ссылочные типы. Динамическое использование памяти: стеки и кучи.
Динамическое использование памяти: стеки и кучи
Все типы в C# разделяются на две основные разновидности: структурные типы (value- based) или типы значенийи ссылочные типы (reference- based).
Типы значений (структурные) можно разделить на три группы: простые (встроенные), структуры (struct)) и перечисления (enum). При присвоении одного структурного типа другому присваивается не сам тип (как область памяти), а его побитовая копия. Выполнение операций с использованием типов значений происходит быстрее, чем при использовании ссылочных типов, так как для них выделяется память из стека.
Стек в программировании имеет два смысла:
1. Как структура данных для их хранения во время работы программы (коллекция Stack, обобщенное программирование).
2. Зарезервированная область оперативной памяти для хранения данных во время работы программы.
Во втором случае стек организуется так:
int x = 286;
double y = 13.87;
В стеках применяется принцип обслуживания LIFO (Last In First Out – последний пришел, первым обслужен). Это значит, что данные из стека обслуживаются по порядку, начиная с вершины стека, т.е. сверху вниз.
Ссылочные типы данных (экземпляры классов, массивы, строки, интерфейсы) хранят ссылки (адреса) на данные, вместо действительных значений, и ведут себя по-другому. Память для них выделяется в управляемой (динамической) куче. Ссылочная переменная содержит информацию о месте хранения, т.е. ссылку на данные. Когда ОС запускает приложение на выполнение, среда CLR резервирует непрерывный диапазон адресов памяти, который называется управляемой (динамической)кучей.
Имеется внутренний указатель, содержащий адрес кучи, по которому может быть размещён создаваемый объект. В начальный момент, когда в куче ещё нет ни одного объекта, этот указатель указывает на базовый адрес зарезервированного адресного пространства. После того, когда с помощью оператора new создаётся новый объект, среда CLR проверяет, имеется ли в куче необходимое количество байт для запрашиваемого объекта. Система возвращает значение указателя (адрес) и переводит указатель (указатель инкрементируется) на следующий после последнего объекта адрес. Например, выполнился конструктор класса и адрес присвоился ссылочной переменной.
Объект С |
Объект В |
Объект А |
Указатель NextPtr
Таким образом, внутренний указатель всегда хранит адрес свободной памяти для очередного создаваемого объекта.
Когда создано много объектов, пространство в управляемой куче может закончиться и тогда автоматически запускается программа Сборщик мусора. Она отслеживает, если ли в куче активные ссылки. Если ссылок на объект нет или объекту установлена null-ссылка, то память высвобождается, а объект помечается на удаление.
Можно один ссылочной тип присваивать другому, но они должны быть одного и того же типа (класса): Ob1 = Ob2. После выполнения операции присваивания оба объекта будут ссылаться на один и тот же участок памяти, которая была выделена объекту Ob2. Затем можно воздействовать на эти объекты с использованием методов класса, применяя либо Ob1, либо Ob2.
Сбор мусора и использование деструкторов
Деструктор – это специальный метод, который вызывается перед тем, как объект будет окончательно разрушен системой сбора мусора. Он обеспечивает гарантию «чистой» ликвидации объекта. Деструктор используется для операций освобождения памяти, выделенной для объектов класса.
Формат записи деструктора:
~Имя_класса ( )
{
// тело деструктора
}
Характеристики деструктора:
1) имя деструктора совпадает с именем класса;
2) для деструктора неявно устанавливается спецификатор доступа public;
3) деструктору нельзя передать аргументы, поэтому в классе может быть определён только один деструктор;
4) деструктор не возвращает значения.
Деструктор вызывается в момент, предшествующий процессу утилизации объекта. В теле деструктора указываются действия, которые должны быть выполнены перед разрушением объекта.
Сбор мусора.После генерации машинного кода он выполняется на определённой платформе. На этом этапе осуществляется очень важное действие – сборка мусора.
Управление ресурсами в программе может оказаться довольно сложной задачей. Как правило, в приложении задействовано значительное количество ресурсов, например, записи баз данных, сетевые соединения, пространство экрана монитора и т.д. В ситуации, когда после использования памяти, выделенной для ресурса, она не освобождается, или доступ к ресурсу продолжается, несмотря на освобождение памяти, работа приложения становится нестабильной и непредсказуемой. Для предотвращения подобных проблем разработана программа Сборщик мусора. Сборщик мусора – один из наиболее важных компонентов среды CLR. Он освобождает программиста от утомительной задачи контроля использования и своевременного освобождения памяти.
Однако в некоторых ситуациях нужно разрабатывать собственные деструкторы. Например, если программист установил сетевое соединение и больше не нуждается в нём, то сборщику мусора не будет известно о необходимости разрыва сетевого соединения до освобождения области памяти, выделенной для этого соединения. Поэтому программист должен в программном коде написать, как правильно следует освобождать ресурс. В среде .NET программисты создают такой код с помощью деструктора или методов Close(), Dispose().
Сборщик мусора освобождает память, занимаемую неиспользуемыми объектами. Иными словами, при удалении последней ссылки на объект он становится кандидатом на удаление Сборщиком мусора. Процесс вызова деструктора или прекращение дальнейшего использования объекта называют завершением (finialzation).
Основная проблема сборки мусора заключена в том, что невозможно точно предсказать, когда Сборщик мусора освободит память. Обычно это происходит при недостатке объёма памяти для хранения данных создаваемых объектов. В результате могут возникнуть ситуации, когда деструктор объекта вообще не будет вызван. Даже на этапе завершения работы приложения CLR может пропустить процесс вызова деструктора объекта с целью ускорения завершения работы. Поэтому рекомендуется не выполнять операции освобождения памяти с помощью деструктора. Желательно создать отдельный метод, в который можно добавить операции по завершению работы (например, закрытие файла или обновление записей в базе данных). Затем необходимо определить явный вызов этого метода.
Определение пользовательских методов класса. Спецификаторы доступа к методам класса.Модификаторы ref, out, params параметров методов. Возвращение методами объектов класса. Возвращение методами массивов.