GC & Events

Недавно прочитал книгу Under the Hood of .Net Management от компании Red Gate и хочу всем ее посоветовать к прочтению, так как написана она очень доступно и хорошо, с картинками, примерами и рекомендациями. Вообще тема управления памятью в .Net достаточно интересна и познавательна, но в большинстве ресурсов описание ее идет с какими-то неимоверными сложностями или же с недостаточно наглядными примерами, на мой взгляд. Отчего не остается в голове цельной картины как же все работает в теории, так как авторы книги признаются, что нет точного описания управления памятью и того как сборщик мусора выполняет свою работу. Есть общие положения и структуры, но все остальное очень сложное и в 99,999% случаев не требуется знать всю начинку и как-то подсказывать сборщику мусора как работать. Т.е. в применении к сборщику мусора справедливо высказывание «помогать – только портить».

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

Теория работы сборщика мусора

Буду рассказывать все очень кратко, детали и более подробный рассказ в книге. Итак, при создании все объекты или ссылки на них попадают в стек выполнения программы. Когда сборщик мусора начинает свою работу, то он строит граф объектов в памяти и смотрит, доступны ли объекты из основного стека, есть ли там элементы привязки (root элементы). Если такие элементы есть, то все новые обнаруженные элементы помечаются как поколение 1 (Gen 1). Т.е. все объекты изначально можно рассматривать как объекты нулевого поколения. И если на этом этапе сборки нет привязки, то элемент уничтожается. Если объект выжил после предыдущей сборки мусора, то он переходит в Gen 2. Далее второго поколения ничего нет. Статические классы сразу попадают в поколение 2.

На рисунке ниже показано состояние, когда элементы попали в коллекцию Gen 0.


Пусть мы «удалили» в коде объект В и создали объект С, тогда при следующей сборке будет:

Полная уборка объектов (из всех поколений) происходит достаточно редко и объекты перешедшие в Gen 2 могут оставаться там очень долго.

Отдельным пунктом стоит упомянуть то, что момент начала работы сборщика мусора не детерминирован, можно лишь подписаться на «событие», которое даст знать за относительный промежуток времени, что сборщик мусора начнет свою работу.

Особенности метода Finalize() и IDisposable

Далее, немного углубляясь в тему работы GC, следует рассказать о ситуации, когда происходит работа с классами, которые реализуют методы Finalize() или же деструктор. Ирония заключается в том, что такие классы живут дольше, чем можно умозрительно представить. Наглым образом
возьму картинку из книги, для иллюстрации.

Т.е. при вызове метода Finalize() объект помещается в специальную очередь, где дожидается отдельного потока GC, который будет выполнять методы Finalize(), так как может быть у вас там написано скачать и проиграть три части фильма Матрица в FullHD.

После того, как метод Finalize() запускается, объект помещается в специальную очередь «достижимости».

 

В целом, это не самый оптимальный путь, как утверждают авторы книги и рекомендуют использовать следующий паттерн.

При такой реализации с использованием метода GC.SuppressFinalize(), сборщику мусора сообщается о том, что нет необходимости помещать объект в очередь для объектов с Finalize(). Это сокращает время жизни объекта в процессе сборки мусора и освобождения памяти, когда разработчик вызывает метод Dispose() или же использует конструкцию using(…).

Events

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

Случай 1

Пусть у нас есть некоторый класс, который создает объекты и подписывает их на свое событие.

Не могу сказать, что это редкая ситуация. Вполне рабочий момент, класс-контейнер с помощью сообщений держит связь с содержимым. Итак, что здесь неверно в плане использования сообщений?

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

Какая-либо интеллектуальная автоматизация отписки по удалению объекта никак не повлияет на ситуацию.

Случай 2

Пусть у нас есть некоторый класс, который создает объекты и подписывает их на чужое событие.

В данном случае все намного сложнее и опаснее. С уничтожением объекта класса Root, созданные им объекты FatClass() никуда не исчезнут, а так и будут копиться и висеть гроздью на объекте реализующем интерфейс ISome. Хотя пример и синтетический, но в реальной жизни скорее всего потребуется с удалением объекта класса Root и отписать от объекта ISome всё содержимое класса Root.

Если еще добавить сложности и объекты FatClass в качестве родителя будут иметь объект Root, то Root будет жить, пока не будет удален объект ISome.

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

Случай 3

Идентичный случаю 2, но подписка происходит на события объектов, которые будем содержать в классе Root.

Такое сценарий не несет в себе сайд-эффектов при очистке коллекции.

Случай 4

Некоторая вариация на тему случая 3, когда подписываемся на реакцию от некоторого сервиса.

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

Мысль

Очень хотелось бы придумать способ для автоматической отписки при наступлении каких-либо событий, чтобы в ручную не писать код по удалению подписок. Чтобы одним действием снять зависимости и сделать объект доступным для сборки. Исходя из того, что я надумал экспериментируя перед написанием статьи, получается что это возможно только для случая 1, что не так уж и полезно.

Хотя, если сгенерируемые объекты используются где-то еще, а отписаться надо, то это можно сделать скопом следующим образом:

Вставляете этот кусок в метод Dispose() все события стали null, что говорит о том, что они не имеют подписантов. Сейчас подумал, что может стоит тогда сразу присвоить значение null этим переменным?

Итого

Итогом всего этого будет еще одно предупреждение о том, что надо всегда отписываться от событий, так как это ведет к «утечкам» памяти в том смысле, что память занимается на очень долго, в зависимости от того, что за класс держит подписки.

Еще не забывайте наследовать свои классы от IDisposable и освобождать ресурсы явно.

Так же читайте книжки и кушайте кашу!

 

 

Hard’n’Heavy!

 

5 комментариев на “GC & Events

  1. Больная тема :-(
    Было бы не лишним привести пример с отпиской в Dispose() и его прямого вызова, чтобы читающий понимал, как правильно делать.

    • Собственно последний кусок кода и надо тупо вставить в Dispose().
      А насчет примера с вызовом Dispose() — будет. Или здесь довставлю чуть позже, либо в следующей статье (скорее серии) будет пример. Ща пилю проект уже недели 1,5 по которому в итоге скорее будет серия статей (как минимум две) и видео. В этом проекте опять затрону тему IDisposable.

    • По этому поводу я тоже писал где-то у себя =) Но не спорю, статья хорошая.

Оставить комментарий