Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЭУМК ОСиСПч3 май.doc
Скачиваний:
8
Добавлен:
03.05.2019
Размер:
1.2 Mб
Скачать

Тема 4. Модели управление памятью, механизм сборки мусора

Модели ручной и автоматической утилизации динамической памяти, их сравнительная характеристика

Переменные ссылочного типа (объекты) размещаются в динамической памяти – «куче». Среда исполнения платформы .NET использует управляемую кучу (managed heap). Если при работе программы превышен некий порог расходования памяти, CLR запускает процесс, называемый сборка мусора. Среда исполнения отслеживает все используемые объекты и определяет реально занимаемую этими объектами память. После этого вся оставшаяся память освобождается, то есть помечается как свободная для использования. Освобождая память, CLR заново размещает «уцелевшие» объекты в куче, чтобы уменьшить ее фрагментацию. Ключевой особенностью сборки мусора является то, что она осуществляется средой исполнения автоматически и независимо от основного потока выполнения приложения.

Автоматическая сборка мусора имеет свои преимущества и свои недостатки. В частности, практически исключаются случайные утечки памяти, которые могут вызвать «забытые» объекты. С другой стороны, объект может захватывать некоторые особо ценные ресурсы (например, подключения к базе данных), которые требуется освободить сразу после того, как объект перестает использоваться. В этой ситуации выходом является написание некого особого метода, который содержит код освобождения ресурсов.

Но как гарантировать освобождение ресурсов, даже если ссылка на объект была случайно утеряна? Класс System.Object содержит виртуальный метод Finalize() без параметров. Если пользовательский класс при работе резервирует некие ресурсы, он может переопределить метод Finalize() для их освобождения. Объекты классов, имеющих реализацию Finalize() при сборке мусора обрабатываются особо. Когда CLR распознаёт, что уничтожаемый объект имеет собственную реализацию метода Finalize(), она откладывает уничтожение объекта. Через некоторое время в отдельном программном потоке происходит вызов метода Finalize() и фактическое уничтожение объекта.

Язык C# не позволяет явно переопределить в собственном классе метод Finalize(). Вместо переопределения Finalize() в классе описывается специальный метод, синтаксис которого напоминает синтаксис деструктора языка C++. Имя метода образовывается по правилу ~<имя класса>, метод не имеет параметров и модификаторов доступа. Считается, что модификатор доступа «деструктора» – protected, следовательно, явно вызвать его у объекта нельзя.

Рассмотрим пример класса с «деструктором»:

class ClassWithDestructor {

public string name;

public ClassWithDestructor(string name) {

this.name = name;

}

public void doSomething() {

Console.WriteLine("I'm working...");

}

//Это деструктор

~ClassWithDestructor() {

Console.WriteLine("Bye!");

}

}

Приведем пример программы, использующей описанный класс:

class MainClass {

public static void Main() {

ClassWithDestructor A = new ClassWithDestructor("A");

A.doSomething();

// Сборка мусора запуститься перед

// завершением приложения

}

}

Данная программа выводит следующие строки:

I'm working...

Bye!

Проблема с использованием метода-«деструктора» заключается в том, что момент вызова этого метода сложно отследить. Программист может описать в классе некий метод, который следует вызывать «вручную», когда объект больше не нужен. Для унификации данного решения платформа .NET предлагает интерфейс IDisposable, содержащий единственный метод Dispose(), куда помещается завершающий код работы с объектом. Класс, объекты которого требуется освобождать «вручную», реализовывает интерфейс IDisposable.

Изменим приведенный выше класс, реализовав в нем интерфейс IDisposable:

class ClassWithDestructor : IDisposable {

public string name;

public ClassWithDestructor(string name) {

this.name = name;

}

public void doSomething() {

Console.WriteLine("I'm working...");

}

// Реализуем метод "освобождения"

public void Dispose() {

Console.WriteLine("Dispose called for " + name);

}

~ClassWithDestructor() {

// Деструктор вызывает Dispose "на всякий случай"

Dispose();

Console.WriteLine("Bye!");

}

}

C# имеет специальную обрамляющую конструкцию using, которая гарантирует вызов метода Dispose() для объектов, использующихся в своем блоке. Синтаксис данной конструкции:

using (<имя объекта или объявление и создание объектов>)

<программный блок>

Изменим программу с классом ClassWithDestructor, поместив туда обрамляющую конструкцию using:

class MainClass {

public static void Main() {

using(ClassWithDestructor A =

new ClassWithDestructor("A")) {

A.doSomething();

// компилятор поместит сюда вызов A.Dispose()

}

}

}

Что выведет на консоль данная программа? Ниже представлены выводимые строки с комментариями:

I'm working... – это работа метода A.doSomething()

Dispose called for A – вызывается Dispose() в конце using

Dispose called for A – эта и следующая строка являются

Bye! - результатом работы деструктора

Сборщик мусора представлен классом System.GC. Метод Collect() данного класса вызывает принудительную сборку мусора в программе и может быть вызван программистом. Не рекомендуется пользоваться методом Collect() часто, так как сборка мусор требует расхода ресурсов.

Общие принципы сборки мусора в среде .NET

Программы только выделяют память, не выполняя освобождения; когда не хватает, за один раз проводится операция дефрагментации и упаковки (компактирования), при которой проверяются все указатели, блоки памяти физически переносятся; указатели, которые указывали на этот блок, корректируются. Это налагает следующие требования: в программе должны легко идентифицироваться переменные ссылочных типов данных и переменные всех других типов. Это достигается в .Net путем строгой типизации переменных и хранения этой информации о типах в используемых модулях.

При аппаратных реализациях сборщика мусора разграничение встроено с саму аппаратную архитектуру. Пример: Эльбрус – в самой памяти дополнительные биты помечают байты как указатели.

Проблема при программной реализации - пока памяти много, это эффективно; когда доходит дело до запуска сборщика мусора начинаются проблемы. Во время работы сборщика мусора работа другого кода программы должна быть приостановлена. Сборка мусора запускается, когда память вся заполнена и работы у него много. Программа прерывается на долгое время. Поэтому в .Net память поделена на поколения. Размер 0-го поколения = 256 Кб, 1-го = 2Мб, 2-го = 10Мб.

Алгоритм: в 0-м поколении имеется указатель, новые объекты всегда находятся в 0-м поколении. Когда в поколении не хватает места для выделения очередного объекта, запускается механизм частичной сборки мусора, который делает следующее: проходит по указателям в программе, которые указывают на 0-ое поколение, и определяет, какие блоки в 0-м поколении еще используются, а какие уже нет. Все еще используемые блоки переносятся в 1-ое поколение. Перенос – просто перенос указателя с укладыванием блоков. В программе указатели корректируются автоматически. В результате, 0-ое поколение становится полностью свободным, и обычная работа программы продолжается. Если при переносе блока из 0-го поколения в 1-ое оказывается, что 1-ое поколение переполнилось, выполняется та же операция, что и для 0-го: делается проход по всем указателям, из них отбираются только те, которые указывают не 1-ое поколение. Используемые указатели переносятся из 1-го во 2-ое поколение. Указатели в 1-м поколении корректируются.

Замечания:

  1. 2-ое поколение переполняется реже 1-го

  2. размер 0-го поколения такой, чтобы он помещался в КЭШ 2-го уровня

  3. размер 1-го поколения такой, чтобы он умещался в КЭШ 1-го уровня (КЭШ оперативной памяти)

В связи с этим, работа в 0-м поколении – работа в процессоре.

При выделении: проверка границы (256Кб, 2М, 10М), если больше, сразу выделяем в 3-м. Если 3-ее переполнено, проводим дефрагментацию всего. Если больше 10М, надо всегда запускать сборщик мусора и упаковывать память.

В Java сборщик мусора запускается в фоновом режиме по прошествии какого-то времени. Сборка мусора проходит в отдельном потоке.

Механизм поколений объектов

Вся динамическая память (heap – куча) состоит из 3-х поколений, в общем случае n поколений. Нумерация – 0,1,2. Когда с помощью new создается первый объект, он выделяется в 0-м поколении. При этом выделение происходит так:

0-ое поколение - область памяти, заполняемая до некоторого объема

NextPtr – указатель на следующий первый байт, который будет выделен. Т.е. механизм, как в стеке, заключается просто в продвижении указателя. Поэтому new работает на порядок быстрее, чем new в С++, т.к. при выделении памяти вручную менеджер памяти создает двунаправленные списки свободных блоков внутри этой памяти. У менеджера памяти есть указатели на первый и последний свободные блоки памяти. Проход и поиск по стеку нового блока труднее, чем инкремент.

Завершение объектов в среде .NET

Завершение объектов в .NET происходит автоматически, сборщиком мусора. Сборка мусора всегда выполняется асинхронно в отдельном потоке (у этого потока наивысший приоритет, и другие потоки приостанавливаются). Для отслеживания удаления объект в базовый класс Object введен завершитель – виртуальный метод Finalise( ) . Этот метод без параметров:

Finalise( );

Этот метод (если он есть в объекте, т.е. переопределен в нем) вызывается в потоке сборщика мусора перед тем, как память объекта будет действительно освобождена, но не перед тем, как пропадут все ссылки на объект. Объект может давно не использоваться в программе, а если сборщик мусора не успел удалить объект, то память объекта освобождена не будет. Вызов метода Finalise( ) не гарантируется, т.е. если в программе происходит ошибка и программа завершается досрочно, то метод Finalise( ) у объекта не вызывается. При завершении программы методы Finalise( ) в своей работе ограничены во времени. Если какой-то Finalise( ) работает слишком долго, то исполняющая система не вызовет выполнение других методов Finalise( ). Порядок вызова Finalise( ) произвольный, т.е. если объект будет создан позже, то он может все равно быть удален раньше ( метод Finalise( ) у него может быть вызван раньше). Метод Finalise( ) работает в контексте сборщика мусора. Метод Finalise( ) нельзя использовать для освобождения ресурсов в программе. Если объект управляет ресурсом (например, создает дескриптор для связи с БД), то вызывать Finalise( ) нельзя.

Модель детерминированного освобождения ресурсов в среде .NET

При использовании языка программирования C++ для уничтожения экземпляра класса явно вызывается деструктор этого класса, например:

delete obj;

При создании объекта не в куче, а в стеке его уничтожение все еще остается детерминированным, так как деструктор класса вызывается в тот момент, когда объект выходит за границы области видимости.

Иначе уничтожаются объекты в .NET Framework. Разработчик всегда создает объекты, но никогда не удаляет их — за него это делает сборщик мусора. Основная проблема – когда вызовется этот сборщик мусора. Пока он не приступит к работе, деструктор объекта не будет вызван, и объект не будет уничтожен. Формально в управляемом коде нет такого понятия, как деструктор. Когда разработчик пишет что-то вроде деструктора на С#, в действительности компилятор переопределяет метод Finalize, который класс наследует от System.Object. C# упрощает синтаксис, позволяя писать некоторое подобие деструктора. Но это только ухудшает дело, поскольку предполагается, что это деструктор, а для несведущих разработчиков деструктор означает детерминированное разрушение.

В приложениях ,NET Framework не происходит детерминированного уничтожения, до того момента пока не будет вызвано:

GC.Collect ();

GC — класс из пространства имен System, он обеспечивает программный интерфейс для сборщика мусора. Collect — это статический метод, принудительно инициирующий сборку. Сборка мусора снижает производительность, поэтому, не рекомендуется использовать данный метод уничтожения объектов.

Для решения данной проблемы используется целый набор правил, которые рекомендуется использовать при корректной работе с неуправляемыми ресурсами:

  • Используйте защищенный метод Dispose, в котором параметром является булева переменная. Этим методом освобождайте любые неуправляемые ресурсы (такие, как описатели файлов), инкапсулированные в классе. Если параметр, переданный защищенному Dispose, имеет значение true, вызывайте также Close или Dispose (открытый Dispose наследуется от IDisposable) для любых членов класса (полей), которые являются оболочками неуправляемых ресурсов.

  • Используйте интерфейс IDisposable. Он содержит единственный метод Dispose без параметров. Реализуйте эту версию Dispose («открытый Dispose») с обращением к GC.SuppressFinalize для предотвращения вызова сборщиком мусора метода Finalize. После этого вызовите защищенный Dispose и передайте ему значение true.

  • Переопределите Finalize. Метод Finalize вызывается сборщиком мусора, когда объект «завершен», т. е. уничтожен. В Finalize вызовите защищенный Dispose и передайте ему false. Параметр false очень важен, так как предотвращает попытку защищенного Dispose вызвать Close или открытый Dispose для любых инкапсулированных членов класса, которые могут быть уже завершены, если идет сборка мусора.

  • Если это не лишено смысла семантически (к примеру, если инкапсулированный в классе ресурс можно закрыть наподобие описателя файла), используйте метод Close, вызывающий открытый Dispose.

Пример правильного использования Dispose:

«Мягкие ссылки» и кэширование данных в среде .NET

Мягкая ссылка – ссылка, которая указывает на достижимые данные, которые сборщик мусора в крайнем случае (когда не хватает памяти) может посчитать, освободить и занулить мягкую ссылку.

Пример:

GC при достатке памяти объект не удаляет. А если памяти не хватает, то память, занятая объектом, считается мусором, и перед сбором мусора GC присваивает null мягким ссылкам.

В программе, когда необходимо обратиться к данным, на которые указывает мягкая ссылка:

wr.Target – взятие указателя из w, потом проверяем, что поместили в жесткую ссылку (если null, объект освобожден, и вызываем метод ReloadData).