Уничтожение объектов

Если создание объектов показалось сложным, приготовьтесь к еще худшему. Как известно, среда CLR содержит в себе сборщик мусора, управляющий памятью от вашего имени. Можно создавать любое количество новых объектов и никогда не беспокоиться о явном освобождении занимаемой ими памяти.

Подавляющее большинство ошибок в “родных” приложениях вызвано несоответствиями между выделением и освобождением памяти: эти ошибки имеют одно общее название — утечка памяти.

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

Среда CLR отслеживает каждую ссылку на управляемый объект в системе. Если во время сжатия кучи CLR обнаруживает, что какой-то объект становится недоступным ни по одной ссылке, он помечается для удаления.

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

По завершении работы финализатора память объекта освобождается при следующем проходе сборщика мусора, и объект окончательно уничтожается, уже безвозвратно.

Финализаторы

Писать финализаторы приходится редко, и на то есть немало причин. При чрезмерном применении финализаторы могут снизить производительность CLR, поскольку финализируемые объекты существуют дольше, чем их нефинализируемые собратья. Даже размещение финализируемых объектов обходится дороже.

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

Когда поток финализации проходит по объектам в очереди на финализацию, он вызывает метод Finalize каждого из этих объектов. Метод Finalize представляет собой переопределение виртуального метода из System.Object; однако в С# не разрешается явно переопределять этот метод. Вместо этого должен быть написан деструктор, который выглядит как метод без типа возврата, без модификаторов, без параметров, идентификатором которого служит имя класса с предшествующим символом тильды (~).

Деструкторы не могут вызываться явно в С#, они не наследуются, как не наследуются конструкторы. Класс может иметь только один деструктор.

Когда вызывается финализатор объекта, то вызывается каждый финализатор в цепочке наследования — от последнего к первому.

Рассмотрим следующий пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
public class Base
~Base() {
Console.WriteLine ( "Base.-Base ()" );
}
public class Derived : Base {
-Derived()
{
Console.WriteLine ( "Derived.-Derived()" );
}
public class EntryPoint {
static void Main()
{
Derived derived = new Derived();
}

Как и можно было ожидать, результат выполнения кода будет таким:

1
2
Derived.-Derived()
Base.-Base()

Хотя сборщик мусора теперь решает задачу по очистке памяти и вам не приходится об этом беспокоиться, когда заходит речь об уничтожении объектов, появляется масса новых забот. Ранее уже упоминалось, что финализаторы выполняются в отдельном потоке CLR.

Поэтому код, который объекты выполняют внутри деструктора, должен быть безопасным в отношении потоков, к тому же в финализаторе никогда не следует использовать другие объекты, поскольку в этот момент они могут быть уже финансированы или уничтожены.

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

Это еще одна причина того, почему не следует вводить зависимости между объектами в блоке кода деструктора. После всего сказанного становится ясно, что в финализаторе не должно делаться ничего сложнее простой уборки, если она необходима.

По сути, финализатор должен создаваться только тогда, когда объект имеет дело с неуправляемыми ресурсами некоторого рода. Однако если управление этими ресурсами производится через стандартный дескриптор Win32, настоятельно рекомендуется использовать для управления ими тип SafeHandler.

Написание оболочки вроде SafeHandler — задача непростая, главным образом из-за финализатора и всех тех вещей, вызов которых нужно гарантировать во всех (даже в совершенно непредсказуемых) ситуациях, таких как отсутствие свободной памяти или возникновение неожиданных исключений.

И, наконец, любой объект, имеющий финализатор, должен реализовать шаблон Disposable (Освобождаемый).

Обработка исключений

Важно обратить внимание на поведение исключений внутри контекста финализатора. Если вы пришли из мира С++, то знаете, что разрешать исключениям распространяться за пределы деструктора неправильно, поскольку в определенных ситуациях это может привести к прерыванию работы приложения.

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

В .NET 2.0 это поведение изменилось по сравнению с тем, что было в .NET 1.1. До появления .NET 2.0 необработанные исключения в потоке финализации просто “проглатывались” после уведомления пользователя, а затем процессу было позволено продолжаться.

Опасность такого поведения заключается в том, что система могла после этого работать в полуисправном и несогласованном состоянии. Поэтому лучше уничтожить процесс, чем продолжать его работу с риском причинения большего вреда.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100