Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
DiVM / OSISP / ОCиСП-Часть3 / Теория / Теория (ОСиСП).doc
Скачиваний:
29
Добавлен:
11.05.2015
Размер:
616.96 Кб
Скачать

Управление памятью в .Net

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

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

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

В С++ delete работает в следующих случаях:

1) занесение освобожденного блока в список свободных (два указателя)

2) если есть свободные блоки, надо сделать дефрагментацию, чтобы можно было выделить большой непрерывный участок.

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

Логически в .Net для программы память считается бесконечной, программа только выделяет память, но никогда не освобождает ее (есть только new, но нет delete).

Общий алгоритм работы сборщика мусора.

Программы только выделяют память, не выполняя освобождения; когда не хватает, за один раз проводится операция дефрагментации и упаковки (компактирования), при которой проверяются все указатели, блоки памяти физически переносятся; указатели, которые указывали на этот блок, корректируются. Это налагает следующие требования: в программе должны легко идентифицироваться переменные ссылочных типов данных и переменные всех других типов. Это достигается в .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 сборщик мусора запускается в фоновом режиме по прошествии какого-то времени. Сборка мусора проходит в отдельном потоке.

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

Во время «уплотнения», если mainObj указывает на 1-ое поколение, то объект, на который он указывает, может быть пропущен.

В идентификаторе типа указывается, какие другие типы данных содержатся внутри.

Сборщик мусора свой для каждой программы.

Деструктор в С++ вызывается тогда, когда объект удаляется (delete) или перед выходом из процедуры, в которой он был объявлен.

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

Finalise( );

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

В .Net используется следующий подход: используется интерфейс IDisposable.

Метод Finalise( ) в базовом классе Object является пустым ( или абстрактным). Если этот метод перекрыт в объекте, то при создании этого объекта оператором new ссылка на объект помещается в так называемую «очередь завершения» (finalization queue). Это позволяет сборщику мусора знать, какие объекты требуют завершения, а какие нет. Когда сборщик мусора переносит используемые блоки памяти из одного поколения в другое (например, из 0-го в 1-ое), то он проверяет, не находится ли освобожденный блок (который не надо переносить) в очереди завершения. Если освобожденный объект находится в очереди завершения (на объект нет ссылок, а одна оставшаяся ссылка – это ссылка в очереди завершения), то объект переносится из младшего поколения в старшее (время жизни объекта удлиняется). Если ссылка извлекается из очереди завершения и помещается в очередь freachable queue (очередь искусственно достижимых объектов). Помещение в список finalization queue замедляет создание объекта. В момент сборки мусора проводится анализ, не находится ли объект, для которого освобождается память, в очереди finalization queue. При этом объект переносится из младшего поколения в более старшее (из 0-го в 1-ое). Как только в очереди freachable появятся какие-то объекты, просыпается высокоприоритетный поток, который начинает изымать объекты из этой очереди и вызывать у них метод Finalise( ). После вызова метода Finalise( ) с объектом ничего не делается, и он остается в памяти, превращаясь в мусор, т.е. становится недостижимым не через один из указателей. Сборка этого мусора (очистка памяти) произойдет только при следующем запуске сборщика мусора.

К работе метода Finalise( ) предъявляются следующие требования:

1) этот метод всегда работает в выделенном высокоприоритетном потоке

2) вызов метода Finalise( ) абсолютно недетерминирован. Даже если объект имеет внутри себя ссылку (подобъект), то неизвестно, в каком порядке будут вызваны Finalise( ) объекта и подобъекта

3) в методах Finalise( ) нельзя обращаться по своим указателям на другие объекты. Точнее, обращаться можно, но не понятно, в каком состоянии находится этот объект. Таким образом, в методах Finalise( ) лучше вообще не обращаться по указателям

4) метод Finalise( ) не должен выполнять ожидание и не должен зацикливаться, т.к. он выполняется в высокоприоритетном специальном потоке, не должна выполняться синхронизация с другими потоками, никаких ожиданий в асинхронной работе системы. Finalise( ) должен выполняться быстро.

Если вызов Finalise( ) производится в момент закрытия программы (в момент закрытия исполняющая система .Net выполняет сборку мусора), то на работу метода Finalise( ) вводится дополнительное ограничение: на завершение метода Finalise( ) отводится не более 2-х секунд. Если больше 2-секунд, то метод Finalise( ) прерывается, и считается, что он завершился нормально. На работу всех методов Finalise( ) отводится 40 секунд. Если больше 40-ка секунд, то никакие другие Finalise( ) не вызываются, и программа просто выбрасывается из памяти.

Недостаток использования Finalise( ) : он вызывается недетерминировано. Если программа соединяется с БД (а это ресурс – соединение), и ресурс надо освободить тогда, когда он уже становится ненужным. Пусть клиент закрыл окно программы, где показывается соединение, надо освобождать ресурс (уже не надо). А освобождение ресурсов происходит только, когда запускается сборка мусора. И может быть, что использовано много ресурсов, а реальных только несколько клиентов. Поэтому надо ввести возможность детерминированного освобождения ресурсов, когда мы сами контролируем освобождение ресурсов.

Другой пример – дескрипторы файлов. Надо явно освобождать дескрипторы. Для реализации детерминированного освобождения ресурсов в .Net реализован интерфейс IDisposable. Он состоит из одного метода:

interface IDisposable

{

void Dispose( );

};

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

в списке finalization queue объекты, для которых не были еще использованы методы Finalise( )

в списке freachable queue находятся объекты, для которых сейчас запущен метод Finalise() (поток обработки). Если эта очередь пуста, для всех объектов используется метод Finalise() (объект в этой очереди уже освобожден, на нем нет ссылок)

Сборщик мусора должен освободить все 0-ое поколение, т.е. перенести в 1-ое поколение.

А теперь пример использования интерфейса IDisposable для детерминированного освобождения ресурсов:

Using System;

Class OSHandle: Object, IDisposible

{ private bool disposed = false;

private IntPtr handle = IntPtr.Zero;

public IntPtr Handle

{ get {return handle}

set {handle = value;}

}

public OSHandle ( ) {…}

~OSHandle ( ) { Dispose (false);} // компилятор воспринимает это, как

protected void Dispose ( ) // protected override void Finalise ( );

{ Dispose (true);

GC.SuppressFinalise ( this);

}

рublic void Close ( )

{ Dispose ( ); }

[DllImport (“Kernel32.dll”)]

static extern bool CloseHandle (IntPtr handle);

protected virtual void Dispose (bool disposing)

{

lock (this)

{ if (!disposed)

{ if (disposing)

{ // освобождение по Close( )

}

if (handle != IntPtr.Zero)

{

CloseHandle (handle);

Handle = IntPtr.Zero;

};

disposed = true;

}

}

}

}

Объект OSHandle – для автоматического управления операционной системой.

IntPtr – это представленный как число Int указатель (используется для работы с операционной системой). Имеется внутри поле handle = IntPtr.Zero, есть свойство public IntPtr Handle. В поле private bool disposed есть признак того, что ресурс объекта был освобожден, т.е. что отработал метод Dispose ( ). Этот признак сделан для того, чтобы метод Dispose ( ) можно было применять несколько раз. Это защита от повторного освобождения ресурсов. Есть конструктор OSHandle ( ) и деструктор ~OSHandle ( ) . Деструктор в С# считается перекрытием метода Finalise ( а в С# перекрывать метод Finalise нельзя). Деструктор вызывает специальный метод Dispose ( ) (но не тот, который в интерфейсе IDisposible). Dispose с параметром (bool disposing) – это метод, который мы сами написали для удобства. По ветке работы деструктора пойдем только в том случае, если при использовании OSHandle забыли использовать метод Close.

Виртуальные методы делаются всегда protected и их перекрывают в наследниках (а напрямую никогда не вызывают). Close вызывает Dispose ( ) , который является методом интерфейса IDisposible. Он перекрыт, и вызывает метод Dispose (true) , метод GC.SuppressFinalise ( this) – это, если объект явно удаляется методом Dispose, то объект после отработки метода Dispose ( ) его надо изъять из очереди Finalisation queue (т.к. для объекта уже освобожден ресурс). Это увеличивает скорость работы сборщика мусора. Этот вызов обеспечит то, что деструктор больше не вызывается.

В [ ] записан атрибут. Атрибуты – объекты, которые при компиляции программы заносятся в .exe файл и затем используются. Мы передали dll-библиотеку. DllImport позволяет подключить библиотеку Kernel32.dll и вызвать из нее метод CloseHandle.

В методе Dispose (bool disposing) при помощи lock блокируется объект, чтобы его нельзя вызвать из другого потока.

Если объект не освобожден (!disposed), то освобождается по Close.

После того как пропали ссылки на объект после вызова Dispose( ), сборщик мусора выполнит освобождение памяти объекта без вызова Finalize().

Если программист явно не вызывает Close() (т.е. Dispose( ) явно не вызывается),

{

FileStream fs = new FileStream( ‘’ ’’); // создаем объект

fs.Read; // работаем с файлом

};

то далее по коду (после “};”) файл скорее всего будет открыт, хотя мы и выйдем за пределы области видимости объекта, и будет «висеть», пока не отработает сборщик мусора. И чтобы гарантировать завершение объекта, перед “}” надо явно вызвать метод fs.Close().

Управление владением и освобождением ресурса должно быть отделено от выделения и освобождения памяти (за освобождение памяти отвечает сборщик мусора, за ресурс – метод Dispose() и интерфейс TDisposable).

Пример объекта, который сам не является оболочкой ресурса, а содержит внутри себя объект FileStream.

uses System.IO;

type

TFileWriter = class (TObject, IDisposible)

private

FDisposed : Boolean;

FFileStream : FileStream;

public

constructor Create (const FileName : string);

procedure Dispose;

procedure WriteInt (value : Integer);

end;

constructor TFileWriter.Create (const FileName : string);

begin

inherited Create;

FFileStream := FileStream.Create (FileName, FileAccess.Read, FileShare.Read);

end;

procedure TFileWrite.Dispose;

begin

Monitor.Enter (Self)

try

if not FDisposed then

begin

FFileStream.Close;

FDisposed := True;

end;

finally

Monitor.Exit (Self); // выход из критической секции

end;

end;

procedure TFileWriter.WriteInt (value : Integer);

begin

if Fdisposed then

raise ObjectDisposedExeption.Create (To String);

end;

В Delphi тоже есть деструкторы, но в отличие от С# (там деструктор – перекрытый Finalise()), в Deiphi деструктор не является перекрытием Finalise(),а в том случае, если он не имеет аргументов, называется Destroy.

Деструктор реализует IDisposable автоматически, и его даже не надо указывать в списке. Во время работы программы ведется статистика того, как программа выделяет и освобождает память, чтобы можно было анализировать. Память под поколения может динамически изменяться во время работы (runtime).

GC иногда может отказаться от дефрагментации памяти (для ускорения работы системы в целом).

Очень редко, когда мы точно знаем, что программы переходит из одного режима работы в другой, тогда у GC есть API-интерфейс, который позволяет управлять его работой вручную.

GC.Collect (1); // запускает работу GC

// 1 – номер поколения, с которым начинается работа

0 – только для 0 поколения

1 – для 0 и 1

2 – для 0,1 и 2

без параметров – все поколения

GC.Collect ( ); == GC.Collect (GC.MaxGeneration);

Но эта сборка означает, что будет просто собран мусор (дефрагментация памяти). Если у объекта есть Finalise( ), то запускается Finalise( ).

Чтобы память была уплотнена:

GC.Collect( );

GC.WaitForPendingFinalise( );

GC.Collect( );

При первом вызове Collect кроме сборки мусора вызывается Finalise( ) у объектов, и при 2-ом Collect удаляются эти объекты, т.к. они уже стали мусором.

Современные системы связаны с увеличением количества аппаратных устройств (количество процессоров, ядер внутри одного процессора). Для работы в таких системах .Net имеет усовершенствование:

  1. на каждый параименный поток в программе отводится свое индивидуальное 0-ое поколение. 1-ое и 2-ое – общие. Это позволяет потокам выделять память параллельно, не выполняя синхронизацию.

  2. При работе в многопроцессорной среде система запускает постоянно живущий фоновый поток GC, задачей которого является постоянное построение графа достижимых объектов. Этот поток работает на одном из процессоров, поэтому не влияет на работу программы. Этот поток может решить, нужно ли собирать мусор при очередном переполнении 0-го поколения.

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

Пример:

Object o = new Object( );

WeakReference wr = new WeakReference (o); // создание мягкой ссылки

o = null; // ей присваивается результат создания

//…

o = wr.Target;

if (o == null)

ReloadData (o);

// использование о

(сборщик мусора знает wr)

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

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

MyData o = new MyData (“MyFileName.bin”);

WeakReference wr = new WeakReference (o);

o = null;

// …

o = (MyData) wr.Target;

if (o == null)

o = new MyData (“MyFileName.bin”);

// использование о