Что такое сборщик мусора c
Перейти к содержимому

Что такое сборщик мусора c

Концепции программирования: Сборка мусора

Сегодня мы поговорим о таком принципе программирования как сборка мусора: что это, как это работает, какие алгоритмы используются. Для начала хочу отметить, что есть на свете люди намного умнее меня, которые в мельчайших подробностях расскажут вам, как осуществляется сборка мусора в определенных языках программирования, в чем специфические особенности библиотек для сборки мусора, и так далее. Я же собираюсь дать общую картину этой области разработки. Надеюсь, вы узнаете что-нибудь новое. А если вы искренне заинтересуетесь темой, то потом всегда можно отправиться в гугл и найти все статьи, в которых намного глубже рассматриваются отдельные аспекты сборки мусора. В этой статье мы особо глубоко копать не будем. Но тем не менее давайте приступим!

Что такое сборка мусора?#

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

Когда вы выделяете память, например, создавая переменную, данные попадают либо в стек, либо в кучу (чтобы лучше разобраться, чем они отличаются, читайте статью «Что такое стек и куча»). Когда вы выделяете участок памяти в стеке, вы всегда указываете локальную область видимости, точно зная нужный размер блока памяти. В стек попадают примитивные типы данных, массивы заданного размера и прочее. Стек — автономное хранилище памяти. С ним вам ни о чём не нужно беспокоиться: стек сам оперативно выделяет и чистит память. Для других переменных — объектов, буферов, строк или глобальных переменных — память выделяется в куче.

В отличие от стека куча не автономна. Переменная, отправленная на хранение в кучу, останется там на протяжении всей работы программы и может менять состояние, пока вы вручную не освободите участки памяти, которые больше не нужны. Сборщик мусора — это инструмент, который снимает с разработчика бремя ручного управления кучей. Большинство современных языков, например, Java, .NET framework, Python, Ruby или Go используют сборку мусора. А вот C и C++ нет. В этих языках над разработчикам висит чрезвычайно важная задача собственноручно вычищать память.

Зачем нужна сборка мусора?#

Сборка мусора освобождает разработчика от целого ряда проблем. Во-первых, это утечка памяти. Если программа не освобождает память от ставших ненужными данных регулярно, то чем больше памяти вы будете выделять в кучу, тем больше эта память будет накапливаться. В результате произойдет переполнение кучи. Даже если разработчик старательно чистит хранилище, стоит появиться всего одной переменной, которую долго не удаляют, и всё — получите утечку памяти. А это плохо.

Допустим, утечек памяти у вас нет. Не расслабляйтесь. Что если вы попытаетесь дать ссылку на переменную, которую уже удалили или переместили? Такая ситуация называется висячий указатель. В лучшем случае в ответ вы получите околесицу и, надеюсь, вызовите ошибку проверки вскоре после того, как используете переменную. А в худшем поверх старой области памяти система запишет новые данные, которые будут выдавать на вид правильные (но логически неверные) ответы. Вы вообще не будете понимать, что творится. Именно такие ошибки памяти сложнее всего устранять.

Вот почему сборщики мусора так необходимы. Они разберутся со всеми перечисленными проблемами. Да, работают они не идеально: сборка мусора забирает ресурсы компьютера и часто не так эффективна, чем ручное удаление памяти. Но это достойный инструмент, если вспомнить, от каких бед он спасает.

Как и когда запускается сборка мусора?#

Зависит от алгоритма. Не существует какого-то одного раз и навсегда установленного способа собирать мусор. Как и компиляторы с интерпретаторами, механизмы сборки мусора совершенствуются со временем. Иногда сборщик мусора работает с заранее определенными временными интервалами, а иногда он ждёт определенных условий, которые должны возникнуть до его запуска. Сборщик мусора почти всегда работает в отдельном потоке в тандеме с программой. В зависимости от реализации языка программирования сборщик мусора либо приостанавливает программу, чтобы разом вымести весь мусор (такую остановку называют stop-the-world или пусть-весь-мир-подождет), либо удаляет память небольшими партиями, либо работает параллельно с программой.

Углубляться дальше — значит разбирать сборку мусора на конкретных языках, а это не наша цель. Поэтому переходим к основным алгоритмам сборки мусора.

Алгоритмы сборки мусора#

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

Подсчёт ссылок#

Подсчёт ссылок, пожалуй, является основной формой сборки мусора. Этот алгоритм легче всего реализовать самостоятельно. Работает он так: всякий раз, когда вы ссылаетесь на область памяти в куче, счётчик для этого конкретного объекта памяти прирастает на единицу. Когда вы удаляете ссылку, счётчик уменьшается на единицу. Когда счётчик на нуле, сборщик мусора вычищает этот объект памяти.

Одно из важных преимуществ сборки мусора с помощью подсчёта ссылок заключается в том, что системе всегда понятно, когда есть мусор (когда на счётчике ноль). Но есть у этого способа и крупные минусы. Циклические ссылки просто-напросто невозможно вычистить. Если объект А ссылается на объект B, а объект В — обратно на А, сборщик мусора, работающий по принципу подсчёта ссылок, их никогда в жизни не удалит. А ещё подсчёт ссылок — очень неэффективный с точки зрения производительности метод, потому что по каждому объекту памяти постоянно идет работа счётчиков.

Из-за указанных недостатков современные сборщики мусора построены на других алгоритмах.

Пометка-очистка (Mark-Sweep)#

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

Самая простая форма такого алгоритма — примитивная пометка-очистка, при которой для каждого выделенного блока памяти хранится специальный бит для сборки мусора. Алгоритм дважды пропускает через себя все объекты: сначала сборщик по биту находит и помечает все пассивные объекты памяти, а потом удаляет их.

Пометка-очистка эффективнее подсчёта ссылок, потому что тут не нужно следить за счетчиками. А ещё такой метод позволяет удалять циклические ссылки. Однако примитивная пометка-очистка — это классический пример сборки мусора по принципу stop-the-world: пока выполняется алгоритм, вся программа вынуждена приостановить работу (не примитивные алгоритмы собирают мусор частями или параллельно с программой). Поскольку отслеживающая сборка мусора может осуществляться в любое время, вам не удастся предугадать, когда программа в очередной раз встанет. Кроме того, алгоритм дважды перебирает память кучи, а это еще больше замедляет программу. А еще при пометке-очистке не приводится в порядок фрагментированная память. Приведу наглядную аналогию. Представьте, что динамическая память (куча) — это цельная сетка. После метода пометки-очистки она будет выглядеть, как очень неудачная игра в Тетрис. Из-за фрагментации впоследствии снижается эффективность выделения памяти в куче. Так что этот алгоритм еще предстоит оптимизировать.

Пометка-сжатие (Mark-Compact)#

Алгоритмы типа пометка-сжатие используют ту же логику, что пометка-очистка, с добавлением ещё как минимум одного действия с отмеченной областью памяти. Делается это, чтобы сжать и дефрагментировать память. Такой метод решают проблему с фрагментацией, которая произошла в результате пометки-очистки. В результате выделение памяти происходит эффективнее — по принципу выбивания (bump allocation), который похож на работу стека («последним пришёл — первым вышел»). С другой стороны, из-за дополнительных итераций пометка-сжатие выполняется и обрабатывается дольше.

Копирование#

Копирование (также известное как алгоритм Чейни) чем-то похоже на пометку-сжатие. Однако вместо того, чтобы по несколько раз перебирать одну и ту же область памяти, алгоритм просто копирует “выжившие” блоки памяти в совершенно новую область после фазы отметки, а это по умолчанию сжимает память. По окончанию копирования старая область памяти очищается, а все ссылки на сохранившуюся память теперь ведут в новые области памяти. При таком подходе сборщикам мусора приходится обрабатывать намного меньше информации. Кроме того, процесс копирования быстрее, чем даже пометка-очистка, потому что фазы очистки тут попросту нет.

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

Поколения объектов#

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

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

Заключение#

Сборка мусора — не самая легкая для понимания тема, в которой и не обязательно досконально разбираться, если вы работаете с современными языками программирования. Но это недостаточно веский довод, чтобы проигнорировать эту тему. Да, сборка мусора почти не имеет отношения к коду, который вы пишете, но этот процесс — неотъемлемая часть любой языковой реализации. А алгоритм сборки мусора, используемый в языке, часто становится основной причиной, по которой люди либо любят, либо терпеть не могут некоторые реализации.

Здорово, если вы дочитали эту статью до конца. Надеюсь, вы узнали что-то новое. Если тема заинтересовала, рекомендую продолжить изучать сборку мусора. Вот увлекательный ресурс (на английском), с которого можно начать. Там есть гифки, иллюстрирующие работы разных алгоритмов сборки мусора.

Пока я собирал информацию для этой статьи, я заметил любопытную вещь. Большинство заметок на тему сборки мусора описывают этот процесс исключительно применительно к основной реализации Java. Конечно, сборка мусора встречается не только в Java, но думаю, такая гегемония именно этого языка в статьях про сборку мусора вызвана тем, что Java очень часто сравнивают с C++, где сборки мусора вообще нет. Надеюсь, что в будущем появится больше постов о том, как сборка мусора осуществляется в других языка. Пока же приходится довольствоваться тем, что есть!

Принципы работы Garbage collection

В этой статье вспомним, что такое Garbage collection (GC), зачем он нужен вообще и какие проблемы решает. Детально рассмотрим режимы работы GC в .NET, поймем, как работает каждый из них, их особенности и различия. Затронем специфику применения некоторых режимов GC в .NET.

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

Введение

Вообще, откуда взялась эта тема? Она появилась из-за поведения наших сервисов, в том числе и на production. Мы увидели, что некоторые приложения начали отнимать 30% CPU. Не могли понять, почему это происходит — ведь по коду все было хорошо. Провели анализ метрик, о которых поговорим позже, и выяснили, что GC потребляет на сборку мусора порядка 30%. И тут возник вопрос — что же с этим делать. Появилось поле для оптимизации. И мы добились хороших результатов, когда после всевозможных манипуляций снизили потребление CPU до 10%, до 5%. Как этого можно добиться, я расскажу ниже.

Когда я задался вопросом и начал готовить эту статью, мне было интересно, а когда у нас появился первый язык, который уже поддерживал сборку мусора. Я даже немного удивился, потому что это был 1964 год. 50 лет назад люди уже задумывались о том, что разработчиков нужно освобождать от занятий с памятью. Это был язык APL. Из языков, которые поддерживают сборку мусора, можно назвать Erlang (1990 год), Eifel, Smalltalk (1972 год), конечно же, C# и любой современный язык, который выходит сейчас, например Go. Это уже must have.

Интересный факт: по исследованиям, разработчики, которые занимаются написанием кода на языках, не поддерживающих сборку мусора, 40% своего продуктивного времени тратят на операции по работе с управлением памятью, что довольно много и, скорее всего, не всегда будет понятно менеджменту.

Что такое Garbage Collection

GC (Garbage Collection — сборка мусора) — высокоуровневая абстракция, которая избавляет разработчиков от необходимости заботиться об освобождении управляемой памяти.

Давайте вспомним основные тезисы по сборке мусора. В .NET сборка мусора основана на трассировке.

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

  • Ссылки на глобальные объекты (хотя в C# они не разрешены, но позволяет размещать глобальные объекты).
  • Ссылки на любые статические объекты или статические поля.
  • Ссылки на локальные объекты в пределах кодовой базы приложения.
  • Ссылки на передаваемые методу параметры объекта.
  • Ссылки на объект, ожидающий финализации.
  • Любые регистры центрального процессора, которые ссылаются на объект.

Во время процесса сборки мусора исполняющая среда будет исследовать объекты в куче, чтобы определить, являются ли они по-прежнему достижимыми (т. е. корневыми) для приложения. Для этого среда CLR будет создавать графы объектов, представляющие все достижимые для приложения объекты. Кроме того, следует иметь в виду, что сборщик мусора никогда не будет создавать граф для одного и того же объекта дважды, избавляя от необходимости выполнения подсчета циклических ссылок, который характерен для программирования в среде COM.

Фазы сборки мусора:

  1. Маркировка (mark phase).
  2. Чистка (sweep phase).
  3. Сжатие (compact phase).

Поколения объектов: нулевое, первое, второе поколение.

Нулевое и первое поколения еще называют эфемерными поколениями. Они нужны для ускорения отклика нашего приложения.

Для работы приложения CLR инициализирует 2 сегмента виртуального адресного пространства — Small object heap (объекты до 85 КБ) и Large object heap (объекты свыше 85 КБ, в некоторых случаях массивы и связанные списки (linked list), не достигшие данного размера).

Конфигурирование GC довольно простое, что отображено на следующем рисунке:

Рисунок 1. App.config

Конфигурировать режимы работы GC можно путем добавления в app.config секции, показанной на слайде выше, с помощью параметров gcConcurrent, gcServer.

Режим рабочей станции

Рисунок 2. Процесс сборки мусора в режиме рабочей станции

Если мы откроем любую книгу по .NET, любую статью по .NET, где у нас описано, как работает Garbage Collection, обычно это звучит так: работает приложение, не хватает памяти для того, чтобы выделить следующий объект, и происходит запуск GC. При этом все активные потоки приложения приостанавливаются. Это самый простой процесс сборки мусора — workstation non-concurrent mode.

Недостатком этого подхода является то, что во время сборки мусора приложение не занимается ничем другим, кроме сборки мусора. Можно ли этого избежать и как-то повысить отклик нашего приложения?

Идея, как повысить производительность приложения, довольно проста: если нулевое и первое поколения собираются очень быстро, то почему бы их не очищать отдельно от второго поколения. Возможно ли так сделать, чтобы при сборке второго поколения, наше приложение и дальше продолжало аллоцировать объекты? Да, возможно.

Параллельная сборка мусора

Рисунок 3. Параллельная сборка мусора

Для этого существует режим параллельной сборки мусора (workstation concurrent GC).

Параллельная сборка мусора в .NET

До выхода .NET 4.0 очистка неиспользуемых объектов проводилась с применением техники параллельной сборки мусора. В этой модели, при выполнении сбора мусора эфемерных объектов, сборщик мусора временно приостанавливал все активные потоки внутри текущего процесса, чтобы приложение не могло получить доступ к управляемой куче вплоть до завершения процесса сборки мусора.

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

Как и оптимизация, параллельная сборка мусора позволяла проводить очистку объектов, которые не были обнаружены ни в одном из эфемерных поколений, в отдельном потоке. Это сокращало (но не устраняло) необходимость в приостановке активных потоков исполняющей средой .NET. Тем более, параллельная сборка мусора позволяла размещать объекты в куче во время сборки объектов неэфемерных поколений.

Фоновая сборка мусора

Рисунок 4. Фоновая сборка мусора

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

Механизм сборки мусора в .NET 4.0 был улучшен так, чтобы на приостановку потока, связанного с деталями сбора мусора, требовалось меньше времени. Благодаря этим изменениям процесс очистки неиспользуемых объектов поколения 0 и 1 стал оптимальным. Он позволяет получать более высокий уровень производительности приложений.

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

Режим сервера

Особенности работы GC в режиме сервера.

  • Сборка выполняется в нескольких выделенных потоках, выполняемых с приоритетом THREAD_PRIORITY_HIGHEST .
  • Для каждого процессора предоставляется куча и выделенный поток, выполняющий сборку мусора, и сборка куч выполняется одновременно. Каждая куча содержит кучу небольших объектов и кучу больших объектов, и все кучи доступны из пользовательского кода. Объекты из различных куч могут ссылаться друг на друга.
  • Так как несколько потоков сборки мусора работают совместно, для кучи одного и того же размера сборка мусора сервера выполняется быстрее сборки мусора рабочей станции.
  • В сборке мусора сервера часто используются сегменты большего размера. Однако обратите внимание, что это только обобщение: размер сегмента зависит от реализации и может изменяться. При настройке приложения не следует делать никаких предположений относительно размера сегментов, выделенных сборщиком мусора.
  • Сборка мусора сервера может оказаться ресурсоемкой операцией. Например, если на компьютере с 4 процессорами выполняется 12 процессов, в каждом из которых применяется сборка мусора сервера, будут использоваться 48 выделенных потоков сборки мусора. В случае высокой загрузки памяти, если все процессы запускают сборку мусора, сборщику мусора понадобится выполнить планирование работы 48 потоков.

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

Рисунок 6. Визуализация работы Garbage Collection в режиме сервера

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

Рисунок 7. Server Background Mode

Начиная с .NET Framework 4.5, фоновая сборка мусора сервера является режимом по умолчанию для сборки мусора сервера. Этот режим функционирует аналогично фоновой сборке мусора рабочей станции, описанной выше, однако с некоторыми отличиями. Для фоновой сборки мусора рабочей станции используется один выделенный поток фоновой сборки мусора, тогда как для фоновой сборки мусора сервера используется несколько потоков — обычно по одному выделенному потоку для каждого логического процессора.

Инструменты мониторинга

GC class

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

Performance Monitor

Одним из самых мощных инструментов для обнаружения проблем с производительностью в Windows являются встроенные счетчики производительности, так называемые Performance counters. Оснастка Performance monitor — основной инструмент для управления ими.

Performance Viewer

Performance Viewer основан на трассировке событий Windows. Чуть позже поговорим о том, что это такое, зачем это нужно и что можно вообще мониторить с его помощью.

SOS Debugging Extension

SOS Debugging Extension стоит отметить, но уже мало кто использует этот инструмент.

dotMemory

Платный представитель от JetBrains. Стоит отметить, что его open source конкуренты на текущий момент мало в чем ему уступают.

Concurrency Vizualizer

Concurrency Vizualizer — расширение для Visual Studio. К мониторингу памяти относится очень косвенно. При этом оно очень информативное, так как позволяет увидеть множество параметров по работе приложения в многопоточной среде. С помощью этой утилиты можно проанализировать, когда потоки приостанавливаются, восстанавливают свою работу и т. д.

Performance Monitor

Рисунок 8. Счетчики Performance Monitor

Какие счетчики (counter) предлагает Performance Monitor? Первый счетчик, на который стоит обратить внимание — это процент времени, которое было потрачено самим GC. Этот счетчик делает замеры между двумя сборками мусора, считает циклы процессора, циклы, которые были потрачены в общем и которые были потрачены на сборку мусора. Например, если между двумя сборками прошел 1 миллион циклов процессора и при этом из них 300 тысяч потрачено на сборку мусора, то, соответственно, наше приложение 30% времени тратит просто для того, чтобы собирать мусор.

На какое значение нужно обращать внимание? Это довольно сложный вопрос. К примеру, мы получили цифру 17. Что мне с этой цифрой делать дальше? Из опыта рекомендую обращать внимание на значение 50%. Если 50% — значит половину времени мы тратим впустую. Если это время тратится еще в дата-центрах, то тратятся деньги. И с этим надо что-то делать. Если мы видим цифру в 10 %, то для того, чтобы опустить ее на 5, нужно потратить столько денег, что даже не стоит в это вкладываться.

Следующий параметр, на который стоит обращать внимание — Allocated bytes/second. Он показывает число байтов в секунду, которые мы можем аллоцировать в памяти. Можем посмотреть, какой размер занимает нулевое поколение, первое, второе поколение, сколько занимает Large Object Heap, как перетекают объекты из нулевого поколения в первое, из первого — во второе, количество выживших объектов и т. д.

Finalization Survivors — это счетчик, который показывает количество объектов, которые ушли с очереди финализации и готовы к тому, чтобы началась их чистка.

Пример, как использовать этот инструмент, показан на рисунке 9.

Рисунок 9. Работа с Performance Monitor

Performance Viewer

На мой взгляд, это один из лучших инструментов на текущий момент. Также радует, что производители уже начали задумываться о том, что же делать с Linux, что очень актуально для приложений, написанных под .NET Core. Уже сейчас на их сайте есть небольшой туториал, как снимать метрики с докер хостов. Надеюсь, они будут продолжать развиваться, и мы получим очень хороший инструмент.

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

Рисунок 10. Работа с Performance Viewer

Инструмент довольно простой (см. рисунок 10): нажимаем collect и собираем нужные нам метрики. Совет для тех, кто будет использовать — не собирайте метрики долго. Сделал большую ошибку: собрал метрики за минуту и потом ждал пока распарсится минут семь, потом бросил. Должно хватить чтобы понять, что происходит с вашим приложением и что с ним делать. Инструмент может показать все, что связано с GC. На слайде выделены данные, которые касаются GC-статистики. Показывается режим GC, в котором запущено наше приложение, время, которое было потрачено на паузы GC, процессорное время, которое было потрачено, количество сборок мусора в каждом из поколений, минимальные паузы, пики.

События трассировки

Если посмотреть определение в MSDN или в литературе, то трассировка событий — это высокоэффективная масштабируемая система трассировки с минимальными затратами ресурсов, которая реализуется в Windows. Если немного заглянуть под капот, то очень грубо говоря, этот процесс выглядит так: мы запускаем трассировку наших приложений, это все ложится в обычные файлики, эти файлики потом парсятся, и мы исследуем, что происходит с нашим приложением.

Что вообще можно мониторить в .NET в среде CLR? GC, Runtime, Exceptions, Thread pool, Stack и т. д. Детально о всех метриках можно почитать здесь.

Cейчас мы рассмотрим Garbage Collection в событиях (event), и что они нам позволяют мониторить. Они нам позволяют собирать сведения, которые как раз и относятся к сборке мусора: когда она началась, когда закончилась, в каком поколении. Как долго длилась не покажут — нужно вычислять самому, и это нетривиальная задача. Нетривиальная потому, что если мы посмотрим на режим рабочей станции, когда у нас нет никаких конкурентных режимов, то там все просто: потоки остановились, приостановились, возобновились. И эту дельту мы можем словить по разнице. Когда мы вспоминаем высокоприоритетную сборку мусора, то тут уже все далеко не тривиально. Поэтому уже лучше пользоваться теми инструментами, которые у нас есть.

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

На рисунке 11 показан небольшой пример, как можно запустить трассировку событий используя TraceEvent Library.

Рисунок 11. Пример кода

На рисунке 12 происходит магия в части того, как мы собираем все эти счетчики. А вот что из всего этого вышло уже отображено на рисунке 13.

Рисунок 12. Пример кода

Мы получили следующую информацию: когда у нас начал выполняться GC и сколько времени заняла пауза на GC, какая по счету сборка мусора, с каким поколением работал GC, в каком режиме работает наше приложение.

GC-визуализация

Рисунок 13. Визуализация GC

Есть довольно интересный блог, который ведет Мэт Уоррен. В нем можно найти очень много интересной и полезной информации: как работает Garbage Collection, что же происходит на самом деле «под капотом».

Рисунок 14. Визуализация GC от Мэта Уоррена

На рисунке 14 отображена визуализация работы GC, основанная на трассировке событий, написанная автором блога. Всем, кому интересно понять, как же работает GC, рекомендую разобраться с ним.

Рисунок 15. Таблица данных тестирования в разных режимах

В следующей таблице собраны метрики, полученные в результате тестирования одного и того же приложения в разных режимах. Было запущено приложение, основной задачей которого была генерация memory-трафика. Что мы видим? Серверный режим, действительно, уменьшает паузы работы GC, уменьшает количество запусков итераций сборки мусора, но это все делается за счет более интенсивного использования CPU и за счет более интенсивного потребления памяти. Об этом всегда нужно помнить. Если у нас десктопное приложение, в котором нам нужен максимальный отклик, то этот режим явно не для него.

Выводы

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

Сборка мусора и финализаторы

В статье «Типы данных» дана характеристика и назначение двух сегментов оперативной памяти: стека (stack) и управляемой кучи (heap), отмечена эффективность стека при управлении памятью (в его работу вы при всем желании вмешаться не сможете). Работа с кучей, в которой размещаются объекты, предполагает использование автоматических сборщиков мусора (Garbage collector). Сбор мусора — это симуляция бесконечной памяти на машине с конечной памятью.
При создании приложений на C# можно полагать, что исполняющая среда .NET будет сама заботиться об управляемой куче без непосредственного вмешательства со стороны программиста. Вам понравится «золотое правило» по управлению памятью в .NET:
Размещайте объект в управляемой куче с использованием ключевого слова new и забывайте об этом.
Программирование в среде с автоматической сборкой мусора значительно облегчает разработку приложений, обязанности по управлению памятью, по сути, сняты с плеч программиста и возложены на CLR-среду.
Далее на качественном уровне – о сборке мусора, видах ресурсов, финализаторах и деструкторах (для начинающих — не самая актуальная тема, но для полноты изложения).

При работе с кучей выделяют управляемые и неуправляемые объекты. Массив, объявленный через ключевое слово new, является примером управляемого ресурса. Примеры неуправляемых ресурсов — это низкоуровневые файловые дескрипторы, низкоуровневые неуправляемые соединения с базами данных, фрагменты неуправляемой памяти. Финализатор (finalizer), как член-функция класса полезен только при использовании неуправляемых ресурсов

Как работает сборщик мусора?

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

Во время процесса сборки мусора исполняющая среда будет исследовать объекты в управляемой куче, чтобы определить, являются ли они по-прежнему достижимыми (т.е. корневыми) для приложения. Для этого среда CLR будет создавать графы объектов, представляющие все достижимые для приложения объекты в куче.

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

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

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

Поколения объектов

При попытке обнаружить недостижимые объекты CLR-среда не проверяет буквально каждый находящийся в куче объект. Очевидно, что на это уходила бы масса времени, особенно в более крупных (реальных) приложениях. Для оптимизации процесса каждый объект в куче относится к определенному «поколению». Смысл в применении поколений выглядит довольно просто: чем дольше объект находится в куче, тем выше вероятность того, что он там и будет оставаться. Например, класс, определенный в главном окне настольного приложения, будет оставаться в памяти вплоть до завершения выполнения программы. С другой стороны, объекты, которые были размещены в куче лишь недавно (как, например, те, что находятся в пределах области действия метода), вероятнее всего будут становиться недостижимым довольно быстро. Исходя из этих предположений, каждый объект в куче относится к одному из перечисленных ниже поколений:

Поколение О Идентифицирует новый только что размещенный объект, который еще никогда не помечался как подлежащий удалению в процессе сборки мусора. Поколение 1 Идентифицирует объект, который уже «пережил» один процесс сборки мусора (был помечен как подлежащий удалению в процессе сборки мусора, но не был удален из-за наличия достаточного места в куче). Поколение 2 Идентифицирует объект, которому удалось пережить более одного прогона сборщика мусора.

Сборщик мусора сначала анализирует все объекты, которые относятся к поколению 0. Если после их удаления остается достаточное количество памяти, статус всех остальных (уцелевших) объектов повышается до поколения 1.

Если все объекты поколения 0 уже были проверены, но все равно требуется дополнительное пространство, проверяться на предмет достижимости и подвергаться процессу сборки мусора начинают объекты поколения 1. Объектам поколения 1, которым удалось уцелеть после этого процесса, затем назначается статус объектов поколения 2. Если же сборщику мусора все равно требуется дополнительная память, тогда на предмет достижимости начинают проверяться и объекты поколения 2. Объектам, которым удается пережить сборку мусора на этом этапе, оставляется статус объектов поколения 2, поскольку более высокие поколения просто не поддерживаются.

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

Метод System.Object.Finalize() и финализатор

Назначение этого метода в C# примерно соответствует деструкторам С++, и он вызывается при сборке мусора для очистки ресурсов, занятых ссылочным объектом. Реализация Finalize() из Object на самом деле ничего не делает и игнорируется сборщиком мусора. Обычно переопределять Finalize() необходимо, если объект владеет неуправляемыми ресурсами, которые нужно освободить при его уничтожении. Сборщик мусора не может сделать это напрямую, потому что он знает только об управляемых ресурсах, поэтому полагается на финализацию, определенную вами.

В редких случаях, когда создается класс C#, в котором используются неуправляемые ресурсы, очевидно, понадобится обеспечить предсказуемое освобождение соответствующей памяти. Для этого необходимо переопределить метод Finalize().

Вместо этого для достижения того же эффекта должен применяться синтаксис деструктора (подобно С++). Объясняется это тем, что при обработке синтаксиса финализатора компилятор автоматически добавляет в неявно переопределяемый метод Finalize() приличное количество требуемых элементов инфраструктуры.

Класс System.GC (Garbage Collector)

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

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

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

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

Сборщик мусора .NET предназначен в основном для того, чтобы управлять памятью вместо разработчиков. Однако в очень редких случаях требуется принудительно запустить сборку мусора с помощью метода GC.Collect(). Примеры таких ситуаций:
1) Приложение приступает к выполнению блока кода, прерывание которого возможным процессом сборки мусора является недопустимым.
2) Приложение только что закончило размещать чрезвычайно большое количество объектов и нуждается в как можно скорейшем освобождении большого объема памяти.

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

Первый подход заключается в переопределении метода System.Object.Finalize() и позволяет гарантировать то, что объект будет очищать себя сам во время процесса сборки мусора (когда бы тот не запускался) без вмешательства со стороны пользователя.

Второй подход предусматривает реализацию интерфейса IDisposable и позволяет обеспечить пользователя объекта возможностью очищать объект сразу же по окончании работы с ним. Однако если пользователь забудет вызвать метод Dispose(), неуправляемые ресурсы могут оставаться в памяти на неопределенный срок.

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

Конструкция Using в C#

(часто встречается при работе с баами данных)

Ключевое слово using упрощает работу с объектами которые реализуют интерфейс IDisposable.
Интерфейс IDisposable содержит один метод .Dispose(), который используется для освобождения ресурсов,
которые захватил объект. При использовании Using не обязательно явно вызывать .Dispose() для объекта.

Пример

При этом компилятор фактически генерирует следующий код:

Заметим, что Using-блоки делают код более читабельным и компактным (некоторый аналог лямбда-операторов).

NEW: Наш Чат, в котором вы можете обсудить любые вопросы, идеи, поделиться опытом или связаться с администраторами.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *