Лучшие методы в .NET: выявление утечек памяти приложения

ОГЛАВЛЕНИЕ

В данной статье рассматривается выявление утечек памяти в приложении .NET.

Загрузить исходный код - WindowsAppMemoryLeak - 34.04 KB

Введение

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

Не используйте диспетчер задач для выявления утечек памяти

Первая и наиболее важная задача – подтвердить наличие утечки памяти. Многие разработчики используют диспетчер задач windows для подтверждения наличия утечки памяти в приложении. Использование диспетчера задач не только вводит в заблуждение, но и не дает достаточной информации о том, где находится утечка памяти.

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

Рабочий набор памяти может быть больше по количеству, чем фактическая используемая память.


Использование счетчиков производительности индивидуальных (закрытых) байтов для выявления утечек памяти

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

Ниже перечислены шаги, которым нужно следовать, чтобы отслеживать индивидуальные байты в приложении при помощи счетчиков производительности:-

  • Запустите приложение, имеющее утечку памяти, и поддерживайте его в рабочем состоянии.
  • Щелкните мышкой по запуску Goto (идти к) и наберите на клавиатуре ‘perfmon’.
  • Удалите все имеющиеся счетчики производительности, выбирая счетчик и удаляя его путем нажатия на кнопку удаления.
  • Нажмите правую кнопку мыши, выберите ‘Add counters (добавить счетчики)’ , выберите ‘process (процесс)’ из объекта производительности.
  • Из списка счетчиков выберите ‘Private bytes (индивидуальные байты)’.
  • Из списка копий выберите приложение, которое вы хотите проверить на утечку памяти.

Если приложение демонстрирует постоянный рост числа индивидуальных байтов, это значит, что в нем есть утечка памяти. На рисунке ниже можно увидеть, как количество индивидуальных байтов постоянно увеличивается, что подтверждает наличие утечки памяти в этом приложении.

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

3-х шаговый процесс изучения утечки памяти

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

  • Какой? -  Сначала нужно попытаться выяснить, каков тип утечки памяти – это управляемая утечка памяти или неуправляемая утечка памяти.
  • Как?  -  Что в действительности вызывает утечку памяти? Это объект соединения, файл какого-то типа, дескриптор которого не закрыт, и т.д.?
  • Где? -  Которая функция / процедура или логика вызывает утечку памяти? 

 


Каков тип утечки памяти?

Перед тем, как пытаться понять, какой тип имеет утечка, попытаемся понять, как выделяется память в приложениях .Net. Приложение .NET имеет два типа памяти - управляемая память и неуправляемая память. Управляемая память контролируется сборкой мусора, в то время как неуправляемая память находится за пределами границ сборщиков мусора.

Сначала нужно удостовериться, каков тип утечки памяти – это управляемая утечка или неуправляемая утечка? Чтобы выяснить, управляемая это  утечка или неуправляемая, нужно измерить два счетчика производительности.

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

Второй счетчик, который нужно добавить, - это ‘Байты во всех кучах’. Выберите ‘память .NET CLR’ в объекте производительности, из списка счетчиков выберите ‘Байты во всех кучах’ и выберите приложение, имеющее утечку памяти.

Индивидуальные байты – это полная память, используемая приложением. Байты во всех кучах – это память, используемая управляемым кодом. Уравнение становится таким, как показано на рисунке ниже.

Неуправляемая память + Байты во всех кучах = индивидуальные байты, поэтому, если нужно найти неуправляемую память, можно вычесть байты во всех кучах из индивидуальных байтов.

Ниже даны два утверждения:

  • Если индивидуальные байты увеличиваются, а байты во всех кучах остаются неизменными, это свидетельствует о неуправляемой утечке памяти.
  • Если байты во всех кучах линейно увеличиваются, это свидетельствует об управляемой утечке памяти.

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

Ниже показан типовой скриншот управляемой утечки. Байты во всех кучах увеличиваются.


Как происходит утечка памяти?

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

Введем утечку неуправляемой памяти, вызвав функцию ‘Marshal.AllocHGlobal’. Эта функция выделяет неуправляемую память и таким образом вводит утечку неуправляемой памяти в приложение. Эта команда выполняется в пределах таймера такое количество раз, чтобы вызвать огромную неуправляемую утечку.

private void timerUnManaged_Tick(object sender, EventArgs e)

{

         Marshal.AllocHGlobal(7000);

}

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

private void timerManaged_Tick(object sender, EventArgs e)

         {

            for (int i = 0; i < 10000; i++)

            {

                Brush obj = new SolidBrush(Color.Blue);

                objBrushes.Add(obj);

            }

        }

Если вам интересно узнать, как в управляемой памяти могут возникать утечки, можете обратиться к слабому обработчику за более подробной информацией http://msdn.microsoft.com/en-us/library/aa970850.aspx  .

Следующий шаг – загрузка инструмента ‘debugdiag’ по адресу http://www.microsoft.com/DOWNLOADS/details.aspx?FamilyID=28bd5941-c458-46f1-b24d-f60151d875a3&displaylang=en 

Запустите инструмент для диагностики при отладке и выберите ‘Утечка памяти и дескриптора’ нажмите следующее.

 

Выберите процесс, в котором вы хотите обнаружить утечку памяти.

В конце выберите ‘Активировать правило сейчас’.

 

Теперь оставьте приложение в работающем состоянии, и инструмент ‘Debugdiag’ будет выполняться во внутренней части, отслеживая выделение памяти.

 

После завершения нажмите на ‘начать анализ’ и позвольте инструменту выполнить анализ.

 

Вы должны получить подробный HTML отчет, показывающий, как была выделена неуправляемая память. В нашем коде была выделена огромная неуправляемая память при помощи ‘AllochGlobal’, что показано в отчете ниже.

Тип

Описание

  Предупреждение

mscorlib.ni.dll отвечает за выделение 3.59 Мбайт невыполненной памяти. Следующие 2 функции потребили больше всего памяти:

System.Runtime.InteropServices.Marshal.AllocHGlobal(IntPtr): выделено 3.59 Мбайт невыполненной памяти.

 Предупреждение

ntdll.dll отвечает за выделение 270.95 мегабайт невыполненной памяти. Следующие 2 функции потребили больше всего памяти:

ntdll!RtlpDphNormalHeapAllocate+1d: выделено 263.78 килобайт невыполненной памяти.
ntdll!RtlCreateHeap+5fc: выделено 6.00 килобайт невыполненной памяти.

Утечка управляемой памяти кистей показана при помощи ‘GdiPlus.dll’ в следующем HTML отчете.

Тип

Описание

 Предупреждение

GdiPlus.dll отвечает за выделение 399.54 килобайт невыполненной памяти.
Следующие 2 функции потребили больше всего памяти:

GdiPlus!GpMalloc+16: выделено 399.54 килобайт невыполненной памяти.


Где находится утечка памяти?

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

Например, из отчета ясно, что ‘AllocHGlobal’ вызывает неуправляемую утечку, в то время как один из объектов GDI вызывает управляемую утечку. Используя эти детали, нужно войти в код, чтобы выяснить, где именно находится утечка.

Исходный код

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