Что такое утечка памяти
Утечки памяти в C++: что это такое и чем они опасны
Разбираемся в трудно уловимых уязвимостях приложений, чтобы всё работало гладко и без тормозов.
Утечка памяти (англ. memory leak) — это неконтролируемое уменьшение свободной оперативной или виртуальной памяти компьютера. Причиной утечек становятся ошибки в программном коде.
В этой статье мы поймём, как появляются утечки и как их устранять.
Как появляются утечки памяти
Любые программы используют в своей работе память, чтобы хранить какие-то данные. В C++ и многих других языках память динамическая. Это значит, что операционная система при запуске программы резервирует какое-то количество ячеек в ОЗУ, а потом выделяет новые, если они нужны.
Создали переменную для числа (int)? Вот тебе 16 бит (4 байта). Нужен массив из ста элементов для больших чисел (long)? Вот тебе ещё 3200 бит (800 байт).
Когда программисту уже не нужен какой-то массив или объект, он должен сказать системе, что его можно удалить с помощью оператора delete[] и освободить память.
Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Однако иногда случаются ошибки, которые приводят к утечкам памяти. Вот одна из них:
На примере в цикле десять раз создаётся новый массив, а его адрес записывается в указатель. Адреса старых массивов при этом удаляются. Поэтому дальше оператор delete[] удаляет только последний созданный массив. Остальные останутся в памяти до тех пор, пока не будет закрыта программа.
Чем опасны утечки памяти
Когда приложение съест всю доступную память, сработает защита ОС и ваша программа аварийно закроется. Однако у утечек могут быть и более опасные последствия.
Например, приложение может работать с каким-нибудь файлом непосредственно перед закрытием. В этом случае файл будет повреждён. Последствия возможны самые разные: от нервного срыва пользователя, если это была презентация, над которой он работал несколько дней, до поломки системы, если это был очень важный файл.
В отдельных случаях утечка памяти одного приложения может привести к последствиям для других работающих приложений. Например, если ваш код изменил или занял память, используемую другой программой.
Может показаться, что раз это «утечка», то что-то случится с вашими данными. На самом деле утекает именно свободная память, а не её содержимое.
Как бороться с утечками памяти
Если у вас есть доступ к исходникам, то изучите код, чтобы определить, нет ли там утечек. Вручную делать это бессмысленно, особенно если проект большой, поэтому обратимся к отладчику использования памяти (англ. memory debugger).
Если вы пользуетесь IDE вроде Visual Studio, то там должен быть встроенный отладчик. Есть и сторонние инструменты вроде GDB или LLDB. Отладчик покажет, какие данные хранит программа и к каким ячейкам имеет доступ.
Ловим утечки памяти в С/С++
Приветствую вас, Хабровчане!
Сегодня я хочу немного приоткрыть свет над тем, как бороться с утечкой памяти в Си или С++.
На Хабре уже существует две статьи, а именно: Боремся с утечками памяти (C++ CRT) и Утечки памяти в С++: Visual Leak Detector. Однако я считаю, что они недостаточно раскрыты, или данные способы могут не дать нужного вам результата, поэтому я хотел бы по возможности разобрать всем доступные способы, дабы облегчить вам жизнь.
Windows — разработка
Начнем с Windows, а именно разработка под Visual Studio, так как большинство начинающих программистов пишут именно под этой IDE.
Для понимания, что происходит, прикладываю реальный пример:
А также есть Student.h и Student.c в котором объявлены структуры и функции.
Есть задача: продемонстрировать отсутствие утечек памяти. Первое, что приходит в голову — это CRT. Тут все достаточно просто.
В начало файла, где находится main, необходимо добавить этот кусок кода:
В итоге, в режиме 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-семейства).
Я написал небольшую программу, которая заполняет динамический массив, но при этом, не очищается память:
Конечно, тут не указана строка, однако уже указана функция, что не может не радовать.
Есть альтернативы valgrind’у, такие как strace или Dr.Memory, но я ими не пользовался, да и они применяется в основном там, где valgrind бессилен.
Выводы
Я рад, что мне довелось столкнуться с проблемой поиска утечки памяти в Visual Studio, так как я узнал много новых инструментов, когда и как ими пользоваться и начал разбирать, как работают эти инструменты.
Спасибо вам за внимания, удачного написания кода вам!
Для будущих студентов курса «Разработчик C#» и всех интересующихся подготовили перевод полезного материала.
Также приглашаем поучаствовать в открытом вебинаре на тему «Методы LINQ, которые сделают всё за вас» — на нем участники обсудят шесть представителей семейства технологий LINQ, три составляющих основной операции запроса, отложенное и немедленное выполнение, параллельные запросы.
Любой, кто работал на крупном корпоративном проекте, знает, что утечки памяти подобны крысам в большом отеле. Вы можете не замечать их, когда их мало, но вы всегда должны быть начеку на случай, если они расплодятся, проберутся на кухню и загадят все вокруг.
В среде со сборкой мусора термин «утечка памяти» представляется немного контринтуитивным. Как может произойти утечка памяти, когда есть сборщик мусора (GC — garbage collector), который берет на себя задачу высвобождения памяти?
На это есть две основные связанные между собой причины. Первая основная причина — это когда у вас есть объекты, на которые все еще ссылаются, но которые фактически не используются. Поскольку на них ссылаются, сборщик мусора не будет их удалять, и они останутся нетронутыми навсегда, занимая память. Это может произойти, например, когда вы подписываетесь на event и никогда не отменяете подписку.
Давайте же перейдем к моему списку лучших практик:
1. Обнаружение утечек памяти с помощью окна средств диагностики
Если вы перейдете в Debug | Windows | Show Diagnostic Tools, вы увидите это окно. Как и я когда-то, вы, вероятно, уже видели это окно после установки Visual Studio, сразу же закрыли его и никогда больше о нем не вспоминали. Окно средств диагностики может быть весьма полезным. Оно может помочь вам легко обнаружить 2 проблемы: утечки памяти и GC Pressure (давление на сборщик мусора).
Когда у вас есть утечки памяти, график использования памяти процессом (Process Memory) выглядит следующим образом:
По желтым линиям, идущим сверху, вы можете наблюдать, как сборщик мусора пытается высвободить память, но загруженность памяти все-равно продолжает расти.
В случае GC Pressure, график использования памяти процессом выглядит следующим образом:
GC Pressure — это когда вы создаете и удаляете новые объекты настолько быстро, что сборщик мусора просто не успевает за вами. Как вы видите на картинке, объем потребляемой памяти близок к своему пределу, а сборка мусора происходит очень часто.
С помощью этого метода вы не сможете найти определенные утечки памяти, но вы навскидку можете обнаружить, что у вас есть проблема с утечкой памяти, что само по себе уже несет пользу. В Visual Studio Enterprise окно средств диагностики также включает встроенный профилировщик памяти, который позволяет обнаружить конкретную утечку. Мы поговорим о профилировании памяти в третьем пункте.
2. Обнаружение утечек памяти с помощью диспетчера задач, Process Explorer или PerfMon
Второй самый простой способ обнаружить серьезные проблемы с утечками памяти — с помощью диспетчера задач (Task Manager) или Process Explorer (от SysInternals). Эти инструменты могут показать объем памяти, который использует ваш процесс. Если она постоянно увеличивается со временем, возможно, у вас утечка памяти.
PerfMon немного сложнее в использовании, но у него есть хороший график потребления памяти с течением времени. Вот график моего приложения, которое бесконечно выделяет память, не освобождая ее. Я использую счетчик Process | Private Bytes.
Обратите внимание, что этот метод заведомо ненадежен. Вы можете наблюдать увеличение потребления памяти только потому, что еще не отработал сборщик мусора. Также стоит вопрос об общей и приватной памяти, поэтому вы можете упустить утечки памяти и/или диагностировать утечки, которые не являются вашими собственными (объяснение). Наконец, вы можете принять утечку памяти за GC Pressure. В этом случае у вас нет утечек памяти, но вы создаете и удаляете объекты так быстро, что сборщик мусора не поспевает за вами.
Несмотря на недостатки, я упоминаю эту технику, потому что она проста в использовании и иногда является вашим единственным подручным инструментом. Это также хороший индикатор того, что что-то не так (при наблюдении в течение очень длительного периода времени).
3. Использование профилировщика памяти для обнаружения утечек
Профилировщик в работе с утечками памяти подобен ножу шеф-повара. Это основной инструмент для их поиска и исправления. Хотя другие методы могут быть проще в использовании или дешевле (лицензии на профилировщики стоят дорого), все таки стоит овладеть навыком работы хотя с бы один профилировщиком памяти, чтобы при необходимости эффективно решать проблемы утечек памяти.
Все профилировщики работают приблизительно одинаково. Вы можете подключиться к запущенному процессу или открыть файл дампа. Профилировщик создаст снапшот текущей памяти из кучи вашего процесса. Вы можете анализировать снапшот различными способами. Например, вот список всех аллоцированных объектов в текущем снапшоте:
Вы можете увидеть, сколько аллоцировано экземпляров каждого типа, сколько памяти они занимают и путь ссылки на GC Root.
Самый быстрый и полезный метод профилирования — это сравнение двух снапшотов, в которых память должна вернуться в одно и то же состояние. Первый снимок делается перед операцией, а второй после выполнения операции. Например, вы можете повторить эти шаги:
Начните с какого-либо состояния бездействия (Idle state) в вашем приложении. Это может быть Главное меню или что-то в этом роде.
Сделайте снапшот с помощью профилировщика памяти, присоединившись к процессу или сохранив дамп.
Запустите операцию, про которую вы подозреваете, что при ней возникла утечка памяти. Вернитесь в состояние бездействия по ее окончании.
Сделайте второй снапшот.
Сравните оба снапшота с помощью своего профилировщика.
Изучите New-Created-Instances, возможно, это утечки памяти. Изучите «path to GC Root» и попытайтесь понять, почему эти объекты не были освобождены.
Вот отличное видео, где в профилировщике памяти SciTech сравниваются два снапшота, в результате чего обнаруживается утечка памяти:
4. Используйте «Make Object ID» для поиска утечек памяти
Предположим, вы подозреваете, что в определенном классе есть утечка памяти. Другими словами, вы подозреваете, что после выполнения определенного сценария этот класс остается ссылочным и никогда не собирается сборщиком мусора. Чтобы узнать, действительно ли сборщик мусора собрал его, выполните следующие действия:
Поместите точку останова туда, где создается экземпляр класса.
Завершите сценарий, который должен был освободить ваш экземпляр от ссылок.
Спровоцируйте сборку мусора с помощью известных волшебных строчек кода.
Здесь я отлаживаю сценарий с утечкой памяти:
А здесь я отлаживаю аналогичный сценарий, в котором нет утечек памяти:
Вы можете принудительно выполнить сборку мусора, вводя волшебные строки в окне непосредственной отладки, что делает эту технику полноценной отладкой без необходимости изменять код.
5. Избегайте известных способов заиметь утечки памяти
Риск нарваться на утечки памяти есть всегда, но есть определенные паттерны, которые помогут получить их с большей вероятностью. Я предлагаю быть особенно осторожным при их использовании и проактивно проверять утечки памяти с помощью таких методов, как последний приведенный здесь пункт.
Вот некоторые из наиболее распространенных подозреваемых:
Статические переменные, коллекции и, в частности, статические события всегда должны вызывать подозрения. Помните, что все статические переменные являются GC Roots, поэтому сборщик мусора никогда не собирает их.
Кэширование — любой тип механизма кэширования может легко вызвать утечку памяти. Кэширую информацию в памяти, в конечном итоге он переполнится и вызовет исключение OutOfMemory. Решением может быть периодическое удаление старых элементов или ограничение объема кэширования.
Привязки WPF могут быть опасными. Практическое правило — всегда выполнять привязку к DependencyObject или к INotifyPropertyChanged. Если вы этого не сделаете, WPF создаст сильную ссылку на ваш источник привязки (то есть ViewModel) из статической переменной, что приведет к утечке памяти. Дополнительную информацию о WPF утечках можно найти в этом полезном треде StackOverflow.
Захваченные члены. Может быть достаточно очевидно, что метод обработчика событий подразумевает, что на объект ссылаются, но когда переменная захвачена анонимным методом — на нее также ссылаются. Вот пример такой утечки памяти:
Потоки, которые никогда не завершаются. Live Stack каждого из ваших потоков считается GC Root. Это означает, что до тех пор, пока поток не завершится, любые ссылки из его переменных в стеке не будут собираться сборщиком мусора. Это также включает таймеры. Если обработчик тиков вашего таймера является методом, то объект метода считается ссылочным и не собирается. Вот пример такой утечки памяти:
6. Используйте шаблон Dispose для предотвращения утечек неуправляемой памяти
Смысл этого шаблона — разрешить явное удаление ресурсов. А также чтобы добавить гарантии того, что ваши ресурсы будут удалены во время сборки мусора (в Finalizer ), если Dispose() не был вызван.
7. Добавление телеметрии памяти из кода
Иногда вам может понадобиться периодически регистрировать использование памяти. Возможно, вы подозреваете, что на вашем рабочем сервере есть утечка памяти. Возможно, вы захотите предпринять какие-то действия, когда ваша память достигнет определенного предела. Или, может быть, у вас просто есть хорошая привычка следить за своей памятью.
Из самого приложения мы можем получить много информации. Получить текущую используемую память очень просто:
Для получения дополнительной информации вы можете использовать PerformanceCounter — класс, который используется для PerfMon :
Доступна информация с любого счетчика perfMon, чего нам хватит с головой.
Однако вы можете пойти еще дальше. CLR MD (Microsoft.Diagnostics.Runtime) позволяет проверить текущую кучу и получить любую возможную информацию. Например, вы можете вывести все выделенные типы в памяти, включая количество экземпляров, пути к корням и так далее. Вы в значительной степени реализовали профилировщик памяти из кода.
Чтобы получить представление о том, чего можно достичь с помощью CLR MD, ознакомьтесь с DumpMiner Дуди Келети.
Вся эта информация может быть записана в файл или, что еще лучше, в инструмент телеметрии, такой как Application Insights.
8. Тестирование на утечки памяти
Профилактическое тестирование на утечки памяти — незаменимая практика. И это не так уж и сложно. Вот небольшой шаблон, который вы можете использовать:
Заключение
Не знаю, как у вас, но моя цель, поставленная на новый год, такова: лучшее управление памятью.
Я надеюсь, что эта статья принесет вам пользу и я буду рад, если вы подпишетесь на мой блог или оставите комментарий ниже. Любые отзывы приветствуются.
Практическое руководство по борьбе с утечками памяти в Node.js
Утечки памяти похожи на сущности, паразитирующие на приложении. Они незаметно проникают в системы, поначалу не принося никакого вреда. Но если утечка оказывается достаточно сильной — она способна довести приложение до катастрофы. Например — сильно его замедлить или попросту «убить».
Автор статьи, перевод которой мы сегодня публикуем, предлагает поговорить об утечках памяти в JavaScript. В частности, речь пойдёт об управлении памятью в JavaScript, о том, как идентифицировать утечки памяти в реальных приложениях, и о том, как с бороться с утечками памяти.
Что такое утечка памяти?
Утечка памяти — это, в широком смысле, фрагмент памяти, выделенной приложению, который больше не нужен этому приложению, но не может быть возвращён операционной системе для дальнейшего использования. Другими словами это — блок памяти, который захвачен приложением без намерения пользоваться этой памятью в будущем.
Управление памятью
Управление памятью — это механизм выделения системной памяти приложению, которое нуждается в ней, и механизм возврата ненужной памяти операционной системе. Существует множество подходов к управлению памятью. То, какой именно подход применяется, зависит от используемого языка программирования. Вот обзор нескольких распространённых подходов к управлению памятью:
JavaScript — язык, без которого веб-программисты не представляют своей работы, использует идею сборки мусора. Поэтому мы подробнее поговорим о том, как работает этот механизм.
Сборка мусора в JavaScript
Как уже было сказано, JavaScript — это язык, в котором используется концепция сборки мусора. В ходе работы JS-программ периодически запускается механизм, называемый сборщиком мусора. Он выясняет то, к каким участкам выделенной памяти можно получить доступ из кода приложения. То есть — то, на какие переменные имеются ссылки. Если сборщик мусора выясняет, что к какому-то участку памяти больше нет доступа из кода приложения, он освобождает эту память. Вышеописанный подход может быть реализован с помощью двух основных алгоритмов. Первый — это так называемый алгоритм пометок (Mark and Sweep). Он используется в JavaScript. Второй — это подсчёт ссылок (Reference Counting). Он применяется в Python и PHP.
Фазы Mark (пометка) и Sweep (очистка) алгоритма Mark and Sweep
При реализации алгоритма пометок сначала создаётся список корневых узлов, представленных глобальными переменными окружения (в браузере это — объект window ), а затем осуществляется обход полученного дерева от корневых к листовым узлам с пометкой всех встреченных на пути объектов. Память в куче, которая занята непомеченными объектами, освобождается.
Утечки памяти в Node.js-приложениях
К настоящему моменту мы разобрали достаточно теоретических понятий, касающихся утечек памяти и сборки мусора. А значит — мы готовы к тому, чтобы посмотреть на то, как всё это выглядит в реальных приложениях. В этом разделе мы напишем Node.js-сервер, в котором есть утечка памяти. Мы попытаемся выявить эту утечку, используя различные инструменты, а потом её устраним.
▍Знакомство с кодом, в котором есть утечка памяти
В демонстрационных целях я написал Express-сервер, у которого есть маршрут с утечкой памяти. Этот сервер мы и будем отлаживать.
▍Вызов утечки памяти
Для того чтобы справиться с нашей проблемой, мы собираемся заняться отладкой в продакшне. То есть — мы позволим нашему серверу переполнить память во время его реального использования (по мере того, как он будет получать самые разные запросы к API). А после того, как обнаружим подозрительный рост объёма выделяемой им памяти, займёмся отладкой.
▍Дамп кучи
Для того чтобы разобраться с тем, что такое «дамп кучи», нам сначала надо выяснить смысл понятия «куча». Если описать это понятие максимально просто, то окажется, что куча — это то место, куда попадает всё то, для чего выделяется память. Всё это находится в куче до тех пор, пока сборщик мусора не уберёт из неё всё то, что будет признано ненужным. Дамп кучи — это нечто вроде снимка текущего состояния кучи. Дамп содержит все внутренние переменные и переменные, объявленные программистом. В нём представлена вся память, выделенная в куче на момент получения дампа.
В результате, если бы мы могли как-то сравнить дамп кучи только что запущенного сервера с дампом кучи сервера, работающего уже долго и переполняющего память, то мы смогли бы идентифицировать подозрительные объекты, которые не нужны приложению, но не удаляются сборщиком мусора.
Прежде чем продолжать разговор — поговорим о том, как создавать дампы кучи. Для решения этой задачи мы воспользуемся npm-пакетом heapdump, который позволяет программно получить дамп кучи сервера.
Внесём в код сервера некоторые изменения, которые позволят нам воспользоваться данным пакетом:
Если ваш сервер работает в кластере Kubernetes, то вам не удастся, без дополнительных усилий, обратиться именно к тому поду, сервер, работающий в котором, потребляет слишком много памяти. Для того чтобы это сделать, можно воспользоваться перенаправлением портов. Кроме того, так как у вас не будет доступа к файловой системе, который нужен для загрузки файлов дампов, эти файлы лучше будет выгрузить во внешнее облачное хранилище (вроде S3).
▍Выявление утечки памяти
И вот, сервер развёрнут. Он работает уже несколько дней. К нему поступает множество запросов (в нашем случае — лишь запросы одного вида) и мы обратили внимание на рост объёма памяти, потребляемой сервером. Заметить утечку памяти можно, пользуясь инструментами мониторинга наподобие Express Status Monitor, Clinic, Prometheus. После этого мы вызываем API для создания дампа кучи. Этот дамп будет содержать все объекты, которые не смог удалить сборщик мусора.
Вот как выглядит запрос, позволяющий создать дамп:
При создании дампа кучи принудительно запускается сборщик мусора. В результате нам не нужно беспокоиться о тех объектах, которые могут быть удалены сборщиком мусора в будущем, но пока находятся в куче. То есть — об объектах, при работе с которыми утечек памяти не происходит.
После того, как в нашем распоряжении окажутся оба дампа (дамп свежезапущенного сервера и дамп сервера, проработавшего некоторое время), мы можем приступить к их сравнению.
Получение дампа памяти — это блокирующая операция, на выполнение которой нужно много памяти. Поэтому выполнять её надо с осторожностью. Подробнее о возможных проблемах, возникающих в ходе этой операции, можно почитать здесь.
Загрузка дампов памяти на вкладке Memory инструментов разработчика Chrome
После загрузки обоих снимков нужно изменить perspective на Comparison и щёлкнуть по снимку памяти сервера, который проработал некоторое время.
Начало сравнения снимков
Анализ подозрительного массива
Эта методика позволит нам выйти на массив leaks и понять, что именно неправильная работа с ним является причиной утечки памяти.
▍Исправление утечки памяти
Для того чтобы проверить действенность принятых мер, достаточно повторить вышеописанные шаги и снова сравнить снимки кучи.
Итоги
Утечки памяти случаются в разных языках. В частности в — тех, в которых используются механизмы сборки мусора. Например — в JavaScript. Обычно исправить утечку несложно — настоящие сложности возникают лишь при её поиске.
В этом материале вы ознакомились с основами управления памятью, и с тем, как организовано управление памятью в разных языках. Здесь мы воспроизвели реальный сценарий утечки памяти и описали методику поиска и устранения проблемы.
Уважаемые читатели! Сталкивались ли вы с утечками памяти в своих веб-проектах?