Что такое утечка памяти c
Утечки памяти представляют собой наиболее незаметные и сложные для обнаружения ошибки в приложениях C/C++. Утечки памяти появляются в результате неправильного освобождения выделенной памяти. Небольшая утечка памяти сначала может остаться незамеченной, но постепенно может приводить к различным симптомам: от снижения производительности до аварийного завершения приложения из-за нехватки памяти. Приложение, в котором происходит утечка памяти, может использовать всю доступную память и привести к аварийному завершению других приложений, в результате чего может быть непонятно, какое приложение отвечает за сбой. Даже безобидная утечка памяти может быть признаком других проблем, требующих устранения.
Отладчик Visual Studio и библиотека времени выполнения C (CRT) позволяют обнаруживать и выявлять утечки памяти.
Интерпретация отчета об утечке памяти
Если приложение не определяет _CRTDBG_MAP_ALLOC , _CrtDumpMemoryLeaks отображает отчет об утечке памяти, аналогичный следующему:
Если приложение определяет _CRTDBG_MAP_ALLOC , отчет об утечке памяти выглядит следующим образом:
Во втором отчете отображается имя файла и номер строки, в которой впервые было произведено выделение утекающей памяти.
Независимо от того, определен ли _CRTDBG_MAP_ALLOC , в отчете об утечке памяти отображается следующее.
- Номер выделения памяти, в этом примере — 18 .
- Тип блока, в примере — normal .
- Расположение памяти в шестнадцатеричном формате, в этом примере — 0x00780E80 .
- Размер блока, в этом примере — 64 bytes .
- Первые 16 байт данных в блоке, в шестнадцатеричном формате.
Типы блоков памяти: обычные, клиентские или CRT. Обычный блок — это обыкновенная память, выделенная программой. Клиентский блок — особый тип блока памяти, используемой программами MFC для объектов, для которых требуется деструктор. Оператор new в MFC создает либо обычный, либо клиентский блок, в соответствии с создаваемым объектом.
Блок CRT — это блок памяти, выделенной библиотекой CRT для внутреннего использования. Библиотека CRT обрабатывает освобождение этих блоков, поэтому CRT-блоки не будут отображаться в отчете об утечке памяти, если нет серьезных проблем с библиотекой CRT.
Существуют два других типа блоков памяти, которые никогда не отображаются в отчетах об утечке памяти. Свободный блок — это блок памяти, которая была освобождена, поэтому по определению утечки здесь нет. Пропускаемый блок — это память, специально помеченная для исключения из отчета об утечке памяти.
Предыдущие способы выявляют утечки памяти для памяти, выделенной с помощью стандартной функции malloc библиотеки CRT. Однако если программа выделяет память с использованием оператора new C++, то в отчете об утечке памяти вы увидите только имя файла и номер строки, где operator new вызывает _malloc_dbg . Чтобы создать более полезный отчет об утечке памяти, можно написать макрос следующего вида, и в отчете будет указываться строка, в которой было выполнено выделение:
Теперь можно заменить оператор new с помощью макроса DBG_NEW в коде. В отладочных сборках DBG_NEW использует перегрузку глобальных operator new , которая принимает дополнительные параметры для типа блока, файла и номера строки. Перегрузка new вызывает _malloc_dbg для записи дополнительных сведений. Отчеты об утечке памяти показывают имя файла и номер строки, в которой были выделены утечки объектов. Сборки выпуска по-прежнему используют new по умолчанию. Вот пример этого метода:
При выполнении этого кода в отладчике Visual Studio вызов _CrtDumpMemoryLeaks создает отчет в окне вывода, который выглядит аналогичным образом:
Эти выходные данные сообщают о том, что утечка памяти находилась на строке 20 файла debug_new.cpp.
Не рекомендуется создавать макрос препроцессора с именем new или любым другим ключевым словом языка.
2. Захват членов класса в анонимных методах
Довольно очевидно, что использование метода в качестве обработчика события приведет к созданию в обработчике ссылки на объект, содержащий этот метод. Но куда менее очевидно, что то же самое происходит, когда член класса захвачен в анонимном методе.
В этом примере член класса _id захвачен в анонимном методе и, как результат, экземпляр класса хранит ссылку на себя. Это означает, что пока _jobQueue существует и ссылается на анонимный делегат, он [ _jobQueue ] ссылается также и на экземпляр MyClass .
Решение проблемы здесь простое — использовать локальную переменную:
Если присвоить значение локальной переменной, член класса не будет захвачен и вы предотвратите утечку памяти.
Примечание переводчика: если вам не совсем понятна природа возникновения утечки в данном случае, обратите внимание на этот комментарий.
1. Обнаружение утечек памяти с помощью окна средств диагностики
Если вы перейдете в Debug | Windows | Show Diagnostic Tools, вы увидите это окно. Как и я когда-то, вы, вероятно, уже видели это окно после установки Visual Studio, сразу же закрыли его и никогда больше о нем не вспоминали. Окно средств диагностики может быть весьма полезным. Оно может помочь вам легко обнаружить 2 проблемы: утечки памяти и GC Pressure (давление на сборщик мусора).
Когда у вас есть утечки памяти, график использования памяти процессом (Process Memory) выглядит следующим образом:
По желтым линиям, идущим сверху, вы можете наблюдать, как сборщик мусора пытается высвободить память, но загруженность памяти все-равно продолжает расти.
В случае GC Pressure, график использования памяти процессом выглядит следующим образом:
GC Pressure — это когда вы создаете и удаляете новые объекты настолько быстро, что сборщик мусора просто не успевает за вами. Как вы видите на картинке, объем потребляемой памяти близок к своему пределу, а сборка мусора происходит очень часто.
С помощью этого метода вы не сможете найти определенные утечки памяти, но вы навскидку можете обнаружить, что у вас есть проблема с утечкой памяти, что само по себе уже несет пользу. В Visual Studio Enterprise окно средств диагностики также включает встроенный профилировщик памяти, который позволяет обнаружить конкретную утечку. Мы поговорим о профилировании памяти в третьем пункте.
Создание дампа памяти
Выполните приведенную ниже команду, чтобы создать основной дамп в Linux для предварительно запущенного примера целевого объекта отладки:
В результате в той же папке будет создан основной дамп.
Заключение
Надеюсь, что статья и перевод были для вас полезными. Удачного программирования.
Также приглашаем поучаствовать в открытом вебинаре на тему «Методы LINQ, которые сделают всё за вас» — на нем участники обсудят шесть представителей семейства технологий LINQ, три составляющих основной операции запроса, отложенное и немедленное выполнение, параллельные запросы.
Любой, кто работал на крупном корпоративном проекте, знает, что утечки памяти подобны крысам в большом отеле. Вы можете не замечать их, когда их мало, но вы всегда должны быть начеку на случай, если они расплодятся, проберутся на кухню и загадят все вокруг.
В среде со сборкой мусора термин «утечка памяти» представляется немного контринтуитивным. Как может произойти утечка памяти, когда есть сборщик мусора (GC — garbage collector), который берет на себя задачу высвобождения памяти?
На это есть две основные связанные между собой причины. Первая основная причина — это когда у вас есть объекты, на которые все еще ссылаются, но которые фактически не используются. Поскольку на них ссылаются, сборщик мусора не будет их удалять, и они останутся нетронутыми навсегда, занимая память. Это может произойти, например, когда вы подписываетесь на event и никогда не отменяете подписку.
Давайте же перейдем к моему списку лучших практик:
Ложные срабатывания
От переводчика: Представляю вашему вниманию перевод статьи 2019 года. И хоть с момента публикации прошло уже больше двух лет, статья более чем актуальна и врядли утратит свое значение в ближайшие пару лет.
КДПВ
Утечки памяти — довольно коварные сущности. Их можно долго не замечать, пока они медленно убивают приложение. При этом растет потребление памяти, создавая нагрузку на сборщик мусора и проблемы с производительностью. В конце концов приложение просто падает с исключением Out of memory.
Термин утечка памяти в средах со сборщиком мусора может вызывать некоторое недоумение. В конце концов, как может утекать память, если есть сборщик мусора, который следит за её своевременным освобождением?
На это есть 2 основные причины. Первая — это объекты, которые не используются в программе, но на которые еще сохранились ссылки. Из-за того, что в других участках кода на объекты есть ссылки, сборщик мусора не освобождает занятую ими память, так что они сохраняются навсегда, удерживая выделенную под них память. Так происходит, например, когда вы регистрируете обработчик события, но не удаляете его. Назовем такие утечки утечками управляемой памяти.
Многие разделяют мнение, что утечки управляемой памяти — это вовсе не утечки, ведь на них все еще есть ссылки и в теории, память все еще можно освободить. Это дискуссионный вопрос, но на мой взгляд, это все же утечки памяти. Они удерживают память, которая не может быть выделена другому экземпляру и в конечном итоге вызывают исключение Out of Memory. В этой статье я буду называть утечки и управляемой, и неуправляемой памяти просто утечками памяти.
Ниже приведено 8 наиболее часто встречающихся причин возникновения утечек. Первые 6 касаются утечек управляемой памяти, оставшиеся 2 — неуправляемой.
Заключение
Не знаю, как у вас, но моя цель, поставленная на новый год, такова: лучшее управление памятью.
Я надеюсь, что эта статья принесет вам пользу и я буду рад, если вы подпишетесь на мой блог или оставите комментарий ниже. Любые отзывы приветствуются.
Сегодня я хочу немного приоткрыть свет над тем, как бороться с утечкой памяти в Си или С++.
На Хабре уже существует две статьи, а именно: Боремся с утечками памяти (C++ CRT) и Утечки памяти в С++: Visual Leak Detector. Однако я считаю, что они недостаточно раскрыты, или данные способы могут не дать нужного вам результата, поэтому я хотел бы по возможности разобрать всем доступные способы, дабы облегчить вам жизнь.
Windows — разработка
Начнем с Windows, а именно разработка под Visual Studio, так как большинство начинающих программистов пишут именно под этой IDE.
Для понимания, что происходит, прикладываю реальный пример:
А также есть Student.h и Student.c в котором объявлены структуры и функции.
Есть задача: продемонстрировать отсутствие утечек памяти. Первое, что приходит в голову — это CRT. Тут все достаточно просто.
А перед return 0 нужно прописать это: _CrtDumpMemoryLeaks(); .
В итоге, в режиме Debug, студия будет выводить это:
Супер! Теперь вы знаете, что у вас утечка памяти. Теперь нужно устранить это, поэтому необходимо просто узнать, где мы забываем очистить память. И вот тут возникает проблема: а где, собственно, выделялась эта память?
После того, как я повторил все шаги, я выяснил, что память теряется где-то здесь:
Но как так — то? Я же все освобождаю? Или нет?
И тут мне сильно не хватало Valgrind, с его трассировкой вызовов.
В итоге, после 15 минут прогугливания, я нашел аналог Valgrind — Visual Leak Detector. Это сторонняя библиотека, обертка над CRT, которая обещала показывать трассировку! Это то, что мне необходимо.
Чтобы её установить, необходимо перейти в репозиторий и в assets найти vld-2.5.1-setup.exe
Правда, последнее обновление было со времен Visual Studio 2015, но оно работает и с Visual Studio 2019. Установка стандартная, просто следуйте инструкциям.
Преимущество этой утилиты заключается в том, что можно не запускать в режиме debug (F5), ибо все выводится в консоль. В самом начале будет выводиться это:
И вот, что будет выдавать при утечке памяти:
Вот, я вижу трассировку! Так, а где строки кода? А где названия функций?
Ладно, обещание сдержали, однако это не тот результат, который я хотел.
Остается один вариант, который я нашел в гугле: моментальный снимок памяти. Он делается просто: в режиме debug, когда доходите до return 0, необходимо в средстве диагностики перейти во вкладку "Использование памяти" и нажать на "Сделать снимок". Возможно, у вас будет отключена эта функция, как на первом скриншоте. Тогда необходимо включить, и перезапустить дебаг.
После того, как вы сделали снимок, у вас появится под кучей размер. Я думаю, это сколько всего было выделено памяти в ходе работы программы. Нажимаем на этот размер. У нас появится окошко, в котором будут содержаться объекты, которые хранятся в этой куче. Чтобы посмотреть подробную информацию, необходимо выбрать объект и нажать на кнопку "Экземпляры представления объекта Foo".
Да! Это победа! Полная трассировка с местоположением вызовов! Это то, что было необходимо изначально.
Linux — разработка
Теперь, посмотрим, что творится в Linux.
В Linux существует утилита valgrind. Чтобы установить valgrind, необходимо в консоли прописать sudo apt install valgrind (Для Debian-семейства).
Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:
Скомпилировав программу с помощью CLang, мы получаем .out файл, который мы подкидываем valgrind'у.
С помощью команды valgrind ./a.out . Как работает valgrind, думаю, есть смысл описать в отдельной статье, а сейчас, как выполнится программа, valgrind выведет это:
Таким образом, valgrind пока показывает, сколько памяти было потеряно. Чтобы увидеть, где была выделена память, необходимо прописать --leak-check=full , и тогда, valgrind, помимо выше описанного, выведет это:
Конечно, тут не указана строка, однако уже указана функция, что не может не радовать.
Есть альтернативы valgrind’у, такие как strace или Dr.Memory, но я ими не пользовался, да и они применяется в основном там, где valgrind бессилен.
Выводы
Я рад, что мне довелось столкнуться с проблемой поиска утечки памяти в Visual Studio, так как я узнал много новых инструментов, когда и как ими пользоваться и начал разбирать, как работают эти инструменты.
Когда приложение ссылается на объекты, которым больше не требуется выполнять нужную задачу, может произойти утечка памяти. Если ссылаться на такие объекты, сборщик мусора не сможет освободить используемую память, что часто приводит к ухудшению производительности и может привести к OutOfMemoryException возникновению исключения.
Здесь используется пример приложения, в котором намеренно происходит утечка памяти. Пример предоставляется для выполнения упражнения. Вы можете проанализировать приложение, в котором также непреднамеренно происходит утечка памяти.
В этом руководстве рассмотрены следующие задачи:
- Изучение использования управляемой памяти с помощью dotnet-counters.
- Создание файла дампа.
- Анализ использования памяти с помощью файла дампа.
5. Избегайте известных способов заиметь утечки памяти
Риск нарваться на утечки памяти есть всегда, но есть определенные паттерны, которые помогут получить их с большей вероятностью. Я предлагаю быть особенно осторожным при их использовании и проактивно проверять утечки памяти с помощью таких методов, как последний приведенный здесь пункт.
Вот некоторые из наиболее распространенных подозреваемых:
Статические переменные, коллекции и, в частности, статические события всегда должны вызывать подозрения. Помните, что все статические переменные являются GC Roots, поэтому сборщик мусора никогда не собирает их.
Кэширование — любой тип механизма кэширования может легко вызвать утечку памяти. Кэширую информацию в памяти, в конечном итоге он переполнится и вызовет исключение OutOfMemory. Решением может быть периодическое удаление старых элементов или ограничение объема кэширования.
Привязки WPF могут быть опасными. Практическое правило — всегда выполнять привязку к DependencyObject или к INotifyPropertyChanged. Если вы этого не сделаете, WPF создаст сильную ссылку на ваш источник привязки (то есть ViewModel) из статической переменной, что приведет к утечке памяти. Дополнительную информацию о WPF утечках можно найти в этом полезном треде StackOverflow.
Захваченные члены. Может быть достаточно очевидно, что метод обработчика событий подразумевает, что на объект ссылаются, но когда переменная захвачена анонимным методом — на нее также ссылаются. Вот пример такой утечки памяти:
Потоки, которые никогда не завершаются. Live Stack каждого из ваших потоков считается GC Root. Это означает, что до тех пор, пока поток не завершится, любые ссылки из его переменных в стеке не будут собираться сборщиком мусора. Это также включает таймеры. Если обработчик тиков вашего таймера является методом, то объект метода считается ссылочным и не собирается. Вот пример такой утечки памяти:
8. Не вызванный метод Dispose
В последнем примере мы добавили метод Dispose для освобождения неуправляемых ресурсов, когда они больше не нужны. Это прекрасно, но что случится, если кто-нибудь использует класс, но не вызовет метод Dispose ?
Конструкция из примера работает на классах, реализующих интерфейс IDisposable и при компиляции автоматически преобразуется в следующий код:
Это довольно удобно, потому что если будет выброшено исключение, метод Dispose все равно будет вызван.
Для достижения наибольшей надежности MSDN предлагает паттерн реализации Dispose. Вот пример его использования:
Использование этого паттерна позволяет гарантировать, что даже если метод Dispose не был вызван явно, то он все равно будет вызван финализатором, когда сборщик мусора решит удалить объект. Если же Dispose вызывался вручную, финализатор для объекта отключается и вызван не будет. Отмена финализатора очень важна, так как его вызов обходится достаточно дорого и может вызывать проблемы с производительностью.
Но учтите, что серебряной пулей майкрософтовский паттерн Dispose не является. Если не вызвать Dispose вручную, и при этом объект не удален сборщиком мусора из-за утечки управляемой памяти, то и неуправляемые ресурсы освобождены не будут.
Задание точек останова для номера выделения памяти
Номер выделения можно использовать для того, чтобы задать точку останова в том месте, где выделяется память.
Установка точки останова для выделения памяти с помощью окна контрольных значений:
Установите точку останова рядом с началом приложения и запустите отладку.
Когда приложение приостанавливается в точке останова, откройте окно Контрольные значения, последовательно выбрав пункты Отладка > Windows > Контрольные значения 1 (или Контрольные значения 2, Контрольные значения 3 или Контрольные значения 4).
В окне Контрольные значения введите _crtBreakAlloc в столбце Имя.
Если используется многопоточная версия DLL-библиотеки CRT (параметр /MD), добавьте контекстный оператор: _crtBreakAlloc
Убедитесь, что отладочные символы загружены. В противном случае _crtBreakAlloc будет отображаться как неидентифицированный.
Нажмите клавишу ВВОД.
Отладчик выполнит оценку вызова и поместит результат в столбец Значение . Это значение будет равно –1, если в местах выделения памяти не задано ни одной точки останова.
В столбце Значение замените отображаемое значение номером выделения памяти, на котором нужно приостановить выполнение.
После задания точки останова для номера выделения памяти можно продолжить отладку. Убедитесь, что соблюдаются те же условия, чтобы номер выделения памяти не изменился. Когда выполнение программы будет приостановлено на заданном выделении памяти, с помощью окна Стек вызовов и других окон отладчика определите условия выделения памяти. Затем можно продолжить выполнение программы и проследить, что происходит с этим объектом и почему выделенная ему память освобождается неправильно.
Иногда может быть полезно задать точку останова по данным на самом объекте. Для получения дополнительной информации см. раздел Использование точек останова.
Точки останова для выделения памяти можно также задать в коде. Можно установить следующие значения:
6. Потоки, которые никогда не останавливаются
Мы уже говорили о том, как работает сборщик мусора и о корневых объектах. Я упоминал, что стек потока считается корневым объектом. Стек потока включает все локальные переменные, а также члены стеков вызовов.
Если вы зачем-то создали бесконечный поток, который ничего не делает и ссылается на объекты, то возникнет утечка памяти. Один из примеров того, как это может легко случиться — неправильное использование класса Timer . Посмотрите на этот код:
Если вы не остановите таймер, он будет бесконечно выполняться в отдельном потоке, удерживая ссылку на MyClass и предотвращая его удаление сборщиком мусора.
Очистка ресурсов
В этом учебнике вы запустили пример веб-сервера. Работа этого сервера должна быть завершена, как описано в разделе Перезапуск неисправного процесса.
4. Используйте «Make Object ID» для поиска утечек памяти
Предположим, вы подозреваете, что в определенном классе есть утечка памяти. Другими словами, вы подозреваете, что после выполнения определенного сценария этот класс остается ссылочным и никогда не собирается сборщиком мусора. Чтобы узнать, действительно ли сборщик мусора собрал его, выполните следующие действия:
Поместите точку останова туда, где создается экземпляр класса.
Наведите курсор на переменную, чтобы открыть всплывающую подсказку отладчика, затем щелкните правой кнопкой мыши и используйте Make Object ID . Вы можете ввести в окне Immediate $1 , чтобы убедиться, что Object ID был создан правильно.
Завершите сценарий, который должен был освободить ваш экземпляр от ссылок.
Спровоцируйте сборку мусора с помощью известных волшебных строчек кода.
5. В появившемся окне непосредственной отладки введите $1 . Если оно возвращает null , значит, сборщик мусора собрал ваш объект. Если нет, у вас утечка памяти.
Здесь я отлаживаю сценарий с утечкой памяти:
А здесь я отлаживаю аналогичный сценарий, в котором нет утечек памяти:
Вы можете принудительно выполнить сборку мусора, вводя волшебные строки в окне непосредственной отладки, что делает эту технику полноценной отладкой без необходимости изменять код.
Перезапуск неисправного процесса
После сбора дампа у вас должно быть достаточно данных для диагностики неисправного процесса. Если неисправный процесс запущен на рабочем сервере, это удачный момент для выполнения краткосрочного исправления проблем путем перезапуска процесса.
Вы уже завершили работу с примером целевого объекта отладки в рамках этого учебника и можете закрыть этот объект. Перейдите к терминалу, с которого запущен сервер, и нажмите клавиши CTRL+C .
Предварительные требования
В этом учебнике используется:
-
или более поздней версии. для проверки использования управляемой памяти. для сбора и анализа файла дампа. для диагностики.
В учебнике предполагается, что пример приложения и инструменты установлены и готовы к использованию.
7. Добавление телеметрии памяти из кода
Иногда вам может понадобиться периодически регистрировать использование памяти. Возможно, вы подозреваете, что на вашем рабочем сервере есть утечка памяти. Возможно, вы захотите предпринять какие-то действия, когда ваша память достигнет определенного предела. Или, может быть, у вас просто есть хорошая привычка следить за своей памятью.
Из самого приложения мы можем получить много информации. Получить текущую используемую память очень просто:
Для получения дополнительной информации вы можете использовать PerformanceCounter — класс, который используется для PerfMon :
5. Некорректная привязка данных в WPF
Привязка данных (Data Binding) в WPF тоже может стать причиной утечек памяти. Главное правило для предотвращения утечек — всегда использовать DependencyObject или INotifyPropertyChanged . Если вы этого не делаете, WPF создает т.н. сильную ссылку (strong reference) на объект, вызывая утечку памяти (более подробное объяснение).
Представленный ниже класс останется в памяти навсегда:
А вот этот класс уже не вызовет утечки:
На самом деле даже не важно, вызываете вы PropertyChanged или нет, главное, что класс реализует интерфейс INotifyPropertyChanged . Это говорит инфраструктуре WPF не создавать сильную ссылку.
Утечки памяти возникают только если используется режим привязки OneWay или TwoWay . Если привязка осуществляется в режиме OneTime или OneWayToSource , то проблемы не будет.
Утечки памяти в WPF также могут возникать, когда происходит привязка коллекций. Если коллекция не реализует INotifyCollectionChanged , вы получите утечку памяти. Вы можете избежать проблемы используя класс ObservableCollection , который этот интерфейс реализует.
7. Не освобожденная неуправляемая память
До сих пор мы говорили только об управляемой памяти, которая освобождается сборщиком мусора. Совсем другое дело — неуправляемая память. Вместо того, чтобы просто избегать ссылок на ненужные объекты, в этом случае вам необходимо явно освобождать память.
Вот простой пример:
В этом примере мы использовали Marshal.AllocHGlobal , чтобы выделить участок неуправляемой памяти (см. документацию в MSDN). Если явно не освободить память при помощи Marshal.FreeHGlobal , она будет считаться выделенной в куче процесса, вызывая утечку памяти, даже после удаления SomeClass сборщиком мусора.
Для предотвращения подобных проблем вы можете добавить в свой класс метод Dispose , в котором очищать неуправляемые ресурсы. Например:
Утечки неуправляемой памяти даже хуже, чем утечки управляемой памяти в связи с фрагментацией. Сборщик мусора умеет дефрагментировать управляемую память, помещая неудаленные объекты рядом, чтобы освободить место для новых данных. В свою очередь, неуправляемая память навсегда привязывается к месту, в котором она выделена.
Сравнение состояний памяти
Другая технология для обнаружения утечек памяти включает получение "снимков" состояния памяти приложения в ключевых точках. Чтобы получить снимок состояния памяти в заданной точке приложения, создайте структуру _CrtMemState и передайте ее функции _CrtMemCheckpoint .
Функция _CrtMemCheckpoint поместит в структуру снимок текущего состояния памяти.
Чтобы вывести содержимое структуры _CrtMemState , передайте ее функции _ CrtMemDumpStatistics :
Функция _ CrtMemDumpStatistics выводит дамп состояния памяти, который выглядит примерно таким образом:
Чтобы определить, произошла ли утечка памяти на отрезке кода, можно сделать снимок состояния памяти перед ним и после него, а затем сравнить оба состояния с помощью функции _ CrtMemDifference :
Функция _CrtMemDifference сравнивает состояния памяти s1 и s2 и возвращает результат в ( s3 ), представляющий собой разницу между s1 и s2 .
Еще один способ поиска утечек памяти заключается в размещении вызовов _CrtMemCheckpoint в начале и конце программы с последующим использованием _CrtMemDifference для сравнения результатов. Если _CrtMemDifference показывает утечку памяти, можно добавить дополнительные вызовы функции _CrtMemCheckpoint , чтобы разделить программу с помощью двоичного поиска, пока не будет найден источник утечки.
4. Кэширование
Разработчики любят кэширование. Действительно, зачем выполнять операцию дважды, если можно выполнить ее один раз и сохранить результат, не так ли?
Это правда, но если кэшировать бесконечно, то в конце концов вы исчерпаете всю доступную память. Посмотрите на этот пример:
Кэширование в этом примере помогает сократить дорогостоящие операции обращения к базе данных, но ценой является захламление памяти.
Для решения проблемы можно использовать следующие практики:
Удалять из кэша данные, которые не используются какое-то время.
Ограничить размер кэша.
Использовать WeakReference для хранения кэшируемых объектов. WeakReference сборщику мусора самостоятельно очищать кэш, что в ряде случаев может оказаться не такой уж и плохой идеей. Сборщик мусора будет перемещать объекты, которые еще используются, в старшие поколения, чтобы держать их в памяти дольше. Это означает, что часто используемые объекты останутся в кэше дольше, тогда как неиспользуемые будут удалены сборщиком мусора без вашего явного участия.
Анализ основного дампа
Теперь, когда у вас есть основной дамп, используйте инструмент dotnet-dump, чтобы проанализировать его:
Где core_20190430_185145 — это имя основного дампа, который нужно проанализировать.
Отобразится командная строка для ввода команд SOS. Как правило, в первую очередь нужно просмотреть общее состояние управляемой динамической памяти:
В нашем примере видно, что большинство объектов принадлежат к типу String либо Customer .
Вы можете повторно выполнить команду dumpheap с помощью таблицы методов, чтобы получить список всех экземпляров String :
Теперь можно использовать команду gcroot в экземпляре System.String , чтобы узнать, как и зачем объект становится корневым. Подождите, так как выполнение этой команды для объема памяти в 30 МБ занимает несколько минут:
Как видно, String непосредственно содержится в объекте Customer и косвенно в объекте CustomerCache .
Вы можете продолжить разгрузку объектов и увидите, что большинство объектов String следуют той же модели. На этом этапе в результате исследования получено достаточно информации, чтобы найти основную причину утечки в коде.
Эта общая процедура позволяет определить источник основных утечек памяти.
6. Используйте шаблон Dispose для предотвращения утечек неуправляемой памяти
Оператор using за кулисами преобразует код в оператор try / finally , где метод Dispose вызывается в finally .
Когда вы сами выделяете неуправляемые ресурсы, вам определенно следует использовать шаблон Dispose . Вот пример:
Смысл этого шаблона — разрешить явное удаление ресурсов. А также чтобы добавить гарантии того, что ваши ресурсы будут удалены во время сборки мусора (в Finalizer ), если Dispose() не был вызван.
GC.SuppressFinalize(this) также имеет важное значение. Она гарантирует, что Finalizer не будет вызван при сборке мусора, если объект уже был удален. Объекты с Finalizer-ами освобождаются иначе и намного дороже. Finalizer добавляется к F-Reachable-Queue , которая позволяет объекту пережить дополнительную генерацию сборщика мусора. Есть и другие сложности.
Изучение использования управляемой памяти
Перед началом сбора данных диагностики с целью поиска основной причины, которая привела к данному сценарию, необходимо убедиться в наличии утечки памяти (или ее увеличения). Для этого используйте dotnet-counters.
Откройте окно консоли и перейдите к каталогу, в который вы скачали и распаковали пример целевого объекта отладки. Запустите целевой объект:
В отдельном окне консоли найдите идентификатор процесса:
Результат должен выглядеть следующим образом:
Теперь проверьте использование управляемой памяти с помощью инструмента dotnet-counters. --refresh-interval задает время между обновлениями (в секундах):
Динамические выходные данные должны выглядеть следующим образом:
Рассмотрите подробнее эту строку:
Как видите, сразу после запуска объем управляемой динамической памяти составляет 4 МБ.
Обратите внимание, что объем использования памяти увеличился до 30 МБ.
Просмотрев данные об использовании памяти, вы можете точно определить, что происходит: утечка или увеличение памяти. Следующим шагом является сбор правильных данных для анализа памяти.
2. Обнаружение утечек памяти с помощью диспетчера задач, Process Explorer или PerfMon
Второй самый простой способ обнаружить серьезные проблемы с утечками памяти — с помощью диспетчера задач (Task Manager) или Process Explorer (от SysInternals). Эти инструменты могут показать объем памяти, который использует ваш процесс. Если она постоянно увеличивается со временем, возможно, у вас утечка памяти.
PerfMon немного сложнее в использовании, но у него есть хороший график потребления памяти с течением времени. Вот график моего приложения, которое бесконечно выделяет память, не освобождая ее. Я использую счетчик Process | Private Bytes.
Обратите внимание, что этот метод заведомо ненадежен. Вы можете наблюдать увеличение потребления памяти только потому, что еще не отработал сборщик мусора. Также стоит вопрос об общей и приватной памяти, поэтому вы можете упустить утечки памяти и/или диагностировать утечки, которые не являются вашими собственными (объяснение). Наконец, вы можете принять утечку памяти за GC Pressure. В этом случае у вас нет утечек памяти, но вы создаете и удаляете объекты так быстро, что сборщик мусора не поспевает за вами.
Несмотря на недостатки, я упоминаю эту технику, потому что она проста в использовании и иногда является вашим единственным подручным инструментом. Это также хороший индикатор того, что что-то не так (при наблюдении в течение очень длительного периода времени).
1. Обработчики событий
Так, если wifiManager определен за пределами MyClass , то мы получили утечку памяти. wifiManager ссылается на экземпляр MyClass , который теперь никогда не будет удален сборщиком мусора.
Что можно сделать в этой ситуации? В вышеуказанной статье описано несколько хороших практик, позволяющих избежать утечек памяти. Не вдаваясь в подробности, вот некоторые из них:
Всегда отписывайтесь от событий.
Используйте паттерны слабых событий (Weak Event Pattern).
Если это возможно, подписывайтесь на события при помощи анонимных методов, не захватывающих других членов класса.
Включение обнаружения утечек памяти
Основным средством для обнаружения утечек памяти является отладчик C/C++ и отладочные функции кучи библиотеки времени выполнения C (CRT).
Чтобы включить все отладочные функции кучи, вставьте в программу C++ следующие операторы в следующем порядке:
Включение файла crtdbg.h сопоставляет функции malloc и free с их отладочными версиями, _malloc_dbg и _free_dbg, которые отслеживают выделение и освобождение памяти. Это сопоставление используется только в отладочных построениях, в которых определен _DEBUG . В окончательных построениях используются первоначальные функции malloc и free .
После того как с помощью этих операторов будут включены отладочные функции кучи, можно поместить вызов _CrtDumpMemoryLeaks перед точкой выхода приложения для отображения отчета об утечке памяти перед завершением работы приложения.
Если приложение имеет несколько выходов, вам не нужно вручную размещать _CrtDumpMemoryLeaks в каждой точке выхода. Для автоматического вызова _CrtDumpMemoryLeaks в каждой точке выхода поместите вызов _CrtSetDbgFlag в начале приложения с помощью следующих битовых полей:
По умолчанию _CrtDumpMemoryLeaks выводит отчет об утечке памяти в область Отладка окна Вывод . Если используется библиотека, она может переустановить вывод в другое расположение.
_CrtSetReportMode можно использовать для перенаправления отчета в другое расположение или обратно в окно вывода, как показано ниже:
3. Статические переменные
Некоторые разработчики считают, что использование статических переменных являются плохой практикой. Тем не менее, говоря об утечках памяти, о них нельзя не упомянуть.
Что считается корневыми объектами?
Cтек исполняющихся потоков.
Управляемые объекты, переданные COM-объектам через Interop.
Это означает, что статические переменные и всё, на что они ссылаются, никогда не будет освобождено сборщиком мусора. Вот пример:
Если вы зачем-то напишите вышеприведенный код, любой экземпляр MyClass навсегда останется в памяти, тем самым вызвав утечку.
3. Использование профилировщика памяти для обнаружения утечек
Профилировщик в работе с утечками памяти подобен ножу шеф-повара. Это основной инструмент для их поиска и исправления. Хотя другие методы могут быть проще в использовании или дешевле (лицензии на профилировщики стоят дорого), все таки стоит овладеть навыком работы хотя с бы один профилировщиком памяти, чтобы при необходимости эффективно решать проблемы утечек памяти.
Все профилировщики работают приблизительно одинаково. Вы можете подключиться к запущенному процессу или открыть файл дампа. Профилировщик создаст снапшот текущей памяти из кучи вашего процесса. Вы можете анализировать снапшот различными способами. Например, вот список всех аллоцированных объектов в текущем снапшоте:
Вы можете увидеть, сколько аллоцировано экземпляров каждого типа, сколько памяти они занимают и путь ссылки на GC Root.
Самый быстрый и полезный метод профилирования — это сравнение двух снапшотов, в которых память должна вернуться в одно и то же состояние. Первый снимок делается перед операцией, а второй после выполнения операции. Например, вы можете повторить эти шаги:
Начните с какого-либо состояния бездействия (Idle state) в вашем приложении. Это может быть Главное меню или что-то в этом роде.
Сделайте снапшот с помощью профилировщика памяти, присоединившись к процессу или сохранив дамп.
Запустите операцию, про которую вы подозреваете, что при ней возникла утечка памяти. Вернитесь в состояние бездействия по ее окончании.
Сделайте второй снапшот.
Сравните оба снапшота с помощью своего профилировщика.
Изучите New-Created-Instances, возможно, это утечки памяти. Изучите «path to GC Root» и попытайтесь понять, почему эти объекты не были освобождены.
Вот отличное видео, где в профилировщике памяти SciTech сравниваются два снапшота, в результате чего обнаруживается утечка памяти:
Читайте также: