Утечки памяти
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Утечки памяти происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов.
Обычно это происходит автоматически (Управление памятью в JavaScript). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях:
- Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.
- Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включённым, чтобы не закрывать браузер с кучей вкладок. Приходит утром – а браузер съел всю память
и рухнул и сильно тормозит.
Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре JavaScript-кода. Мы разберём несколько наиболее частых и важных примеров.
Коллекция утечек в IE
Утечка DOM ↔ JS в IE8-
IE до версии 8 не умел очищать циклические ссылки, появляющиеся между DOM-объектами и объектами JavaScript. В результате и DOM и JS оставались в памяти навсегда.
В браузере IE8 была проведена серьёзная работа над ошибками, но утечка в IE8- появляется, если круговая ссылка возникает «через объект».
Чтобы было понятнее, о чём речь, посмотрите на следующий код. Он вызывает утечку памяти в IE8-:
function leak() < // Создаём новый DIV, добавляем к BODY var elem = document.createElement('div'); document.body.appendChild(elem); // Записываем в свойство жирный объект elem.__expando = < bigAss: new Array(1000000).join('lalala') >; // Создаём круговую ссылку. Без этой строки утечки не будет. elem.__expando.__elem = elem; // Удалить элемент из DOM. Браузер должен очистить память. elem.parentElement.removeChild(elem); >
Полный пример (только для IE8-, а также IE9 в режиме совместимости с IE8):
Поиск, устранение и предупреждение утечек памяти в C# .NET: 8 лучших практик
Для будущих студентов курса «Разработчик C#» и всех интересующихся подготовили перевод полезного материала.
Также приглашаем поучаствовать в открытом вебинаре на тему «Методы LINQ, которые сделают всё за вас» — на нем участники обсудят шесть представителей семейства технологий LINQ, три составляющих основной операции запроса, отложенное и немедленное выполнение, параллельные запросы.

Любой, кто работал на крупном корпоративном проекте, знает, что утечки памяти подобны крысам в большом отеле. Вы можете не замечать их, когда их мало, но вы всегда должны быть начеку на случай, если они расплодятся, проберутся на кухню и загадят все вокруг.
Умение обнаруживать, исправлять и предупреждать утечки памяти — очень важный навык. Здесь я перечислю 8 лучших практик, используемых мной и моими коллегами старшими .NET разработчиками, которые и вдохновили меня написать эту статью. Эти методы научат вас определять, когда в приложении возникает утечка памяти, находить и исправлять ее. Наконец, я включил в статью стратегии для мониторинга и отчета об утечках памяти в уже развернутых программах.
Что из себя представляют утечки памяти в .NET
В среде со сборкой мусора термин «утечка памяти» представляется немного контринтуитивным. Как может произойти утечка памяти, когда есть сборщик мусора (GC — garbage collector), который берет на себя задачу высвобождения памяти?
На это есть две основные связанные между собой причины. Первая основная причина — это когда у вас есть объекты, на которые все еще ссылаются, но которые фактически не используются. Поскольку на них ссылаются, сборщик мусора не будет их удалять, и они останутся нетронутыми навсегда, занимая память. Это может произойти, например, когда вы подписываетесь на event и никогда не отменяете подписку.
Вторая причина заключается в том, что вы каким-то образом выделяете неуправляемую память (без сборки мусора) и не освобождаете ее. Сделать это не так уж и сложно. В самой .NET есть множество классов, которые выделяют неуправляемую память. Практически все, что связано с потоками, графикой, файловой системой или сетевыми вызовами, делает это под капотом. Обычно эти классы реализуют метод Dispose , который освобождает память (мы поговорим об этом позже). Вы можете легко выделить неуправляемую память самостоятельно с помощью специальных классов .NET .Например, Marshal или PInvoke (пример этого будет ниже).
Давайте же перейдем к моему списку лучших практик:
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. Использование профилировщика памяти для обнаружения утечек
Профилировщик в работе с утечками памяти подобен ножу шеф-повара. Это основной инструмент для их поиска и исправления. Хотя другие методы могут быть проще в использовании или дешевле (лицензии на профилировщики стоят дорого), все таки стоит овладеть навыком работы хотя с бы один профилировщиком памяти, чтобы при необходимости эффективно решать проблемы утечек памяти.
Вот несколько довольно известных профилировщиков для .NET: dotMemory, SciTech Memory Profiler и ANTS Memory Profiler. Также есть «бесплатный» профилировщик, если у вас стоит Visual Studio Enterprise.
Все профилировщики работают приблизительно одинаково. Вы можете подключиться к запущенному процессу или открыть файл дампа. Профилировщик создаст снапшот текущей памяти из кучи вашего процесса. Вы можете анализировать снапшот различными способами. Например, вот список всех аллоцированных объектов в текущем снапшоте:

Вы можете увидеть, сколько аллоцировано экземпляров каждого типа, сколько памяти они занимают и путь ссылки на GC Root.
GC Root — это объект, который сборщик мусора не может освободить, поэтому все, на что ссылается GC Root, также не может быть освобождено. Статические и локальные объекты, текущие активные потоки, являются GC Roots. Подробнее об этом читайте в статье «Сборка мусора в .NET».
Самый быстрый и полезный метод профилирования — это сравнение двух снапшотов, в которых память должна вернуться в одно и то же состояние. Первый снимок делается перед операцией, а второй после выполнения операции. Например, вы можете повторить эти шаги:
- Начните с какого-либо состояния бездействия (Idle state) в вашем приложении. Это может быть Главное меню или что-то в этом роде.
- Сделайте снапшот с помощью профилировщика памяти, присоединившись к процессу или сохранив дамп.
- Запустите операцию, про которую вы подозреваете, что при ней возникла утечка памяти. Вернитесь в состояние бездействия по ее окончании.
- Сделайте второй снапшот.
- Сравните оба снапшота с помощью своего профилировщика.
- Изучите New-Created-Instances, возможно, это утечки памяти. Изучите «path to GC Root» и попытайтесь понять, почему эти объекты не были освобождены.
Вот отличное видео, где в профилировщике памяти SciTech сравниваются два снапшота, в результате чего обнаруживается утечка памяти:
4. Используйте «Make Object ID» для поиска утечек памяти
В моей последней статье 5 методов, позволяющих избежать утечек памяти из-за событий в C# .NET, которые вы должны знать, я показал способ найти утечку памяти, поместив точку останова в класс Finalizer. Я покажу вам похожий метод, который еще проще в использовании и не требует изменения кода. Здесь используется функция отладчика Make Object ID и окно непосредственной отладки (Immediate Window).
Предположим, вы подозреваете, что в определенном классе есть утечка памяти. Другими словами, вы подозреваете, что после выполнения определенного сценария этот класс остается ссылочным и никогда не собирается сборщиком мусора. Чтобы узнать, действительно ли сборщик мусора собрал его, выполните следующие действия:
- Поместите точку останова туда, где создается экземпляр класса.
- Наведите курсор на переменную, чтобы открыть всплывающую подсказку отладчика, затем щелкните правой кнопкой мыши и используйте Make Object ID . Вы можете ввести в окне Immediate $1 , чтобы убедиться, что Object ID был создан правильно.
- Завершите сценарий, который должен был освободить ваш экземпляр от ссылок.
- Спровоцируйте сборку мусора с помощью известных волшебных строчек кода.
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
5. В появившемся окне непосредственной отладки введите $1 . Если оно возвращает null , значит, сборщик мусора собрал ваш объект. Если нет, у вас утечка памяти.
Здесь я отлаживаю сценарий с утечкой памяти:
А здесь я отлаживаю аналогичный сценарий, в котором нет утечек памяти:
Вы можете принудительно выполнить сборку мусора, вводя волшебные строки в окне непосредственной отладки, что делает эту технику полноценной отладкой без необходимости изменять код.
Важно: этот метод не работает в отладчике .NET Core 2.X (проблема). Принудительная сборка мусора в той же области, что и выделение объекта, не освобождает этот объект. Вы можете сделать это, приложив немного больше усилий, спровоцировав сборку мусора в другом методе вне области видимости.
5. Избегайте известных способов заиметь утечки памяти
Риск нарваться на утечки памяти есть всегда, но есть определенные паттерны, которые помогут получить их с большей вероятностью. Я предлагаю быть особенно осторожным при их использовании и проактивно проверять утечки памяти с помощью таких методов, как последний приведенный здесь пункт.
Вот некоторые из наиболее распространенных подозреваемых:
- События (Events) в .NET печально известны тем, что вызывают утечки памяти. Вы можете подписаться на событие, даже не подозревая, что это вызовет серьезную утечку памяти. Эта тема настолько серьезная, что я посвятил ей целую статью: 5 методов, позволяющих избежать утечек памяти из-за событий в C# .NET, которые вы должны знать
- Статические переменные, коллекции и, в частности, статические события всегда должны вызывать подозрения. Помните, что все статические переменные являются GC Roots, поэтому сборщик мусора никогда не собирает их.
- Кэширование — любой тип механизма кэширования может легко вызвать утечку памяти. Кэширую информацию в памяти, в конечном итоге он переполнится и вызовет исключение OutOfMemory. Решением может быть периодическое удаление старых элементов или ограничение объема кэширования.
- Привязки WPF могут быть опасными. Практическое правило — всегда выполнять привязку к DependencyObject или к INotifyPropertyChanged. Если вы этого не сделаете, WPF создаст сильную ссылку на ваш источник привязки (то есть ViewModel) из статической переменной, что приведет к утечке памяти. Дополнительную информацию о WPF утечках можно найти в этом полезном треде StackOverflow.
- Захваченные члены. Может быть достаточно очевидно, что метод обработчика событий подразумевает, что на объект ссылаются, но когда переменная захвачена анонимным методом — на нее также ссылаются. Вот пример такой утечки памяти:
public class MyClass < private int _wiFiChangesCounter = 0; public MyClass(WiFiManager wiFiManager) < wiFiManager.WiFiSignalChanged += (s, e) =>_wiFiChangesCounter++; >
- Потоки, которые никогда не завершаются. Live Stack каждого из ваших потоков считается GC Root. Это означает, что до тех пор, пока поток не завершится, любые ссылки из его переменных в стеке не будут собираться сборщиком мусора. Это также включает таймеры. Если обработчик тиков вашего таймера является методом, то объект метода считается ссылочным и не собирается. Вот пример такой утечки памяти:
public class MyClass < public MyClass(WiFiManager wiFiManager) < Timer timer = new Timer(HandleTick); timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); >private void HandleTick(object state) < // do something >
Подробнее об этом читайте в моей статье 8 способов вызвать утечки памяти в .NET.
6. Используйте шаблон Dispose для предотвращения утечек неуправляемой памяти
Ваше приложение .NET постоянно использует неуправляемые ресурсы. Сама платформа .NET в значительной степени полагается на неуправляемый код для внутренних операций, оптимизации и Win32 API. Каждый раз, когда вы используете потоки, графику или файлы, например, вы, вероятно, исполняете неуправляемый код.
Классы .NET Framework , использующие неуправляемый код, обычно реализуют IDisposable . Это связано с тем, что неуправляемые ресурсы должны быть явно освобождены, а это происходит в методе Dispose . Ваша единственная задача — не забыть вызвать метод Dispose . Если возможно, используйте для этого оператор using .
public void Foo() < using (var stream = new FileStream(@"C:\Temp\SomeFile.txt", FileMode.OpenOrCreate)) < // do stuff >// stream.Dispose() will be called even if an exception occurs
Оператор using за кулисами преобразует код в оператор try / finally , где метод Dispose вызывается в finally .
Но даже если вы не вызовете метод Dispose , эти ресурсы будут освобождены, поскольку классы .NET используют шаблон Dispose. Это означает, что если Dispose не был вызван раньше, он вызывается из Finalizer , когда объект собирается сборщиком мусора. То есть, если у вас нет утечки памяти и действительно вызывается Finalizer .
Когда вы сами выделяете неуправляемые ресурсы, вам определенно следует использовать шаблон Dispose . Вот пример:
public class MyClass : IDisposable < private IntPtr _bufferPtr; public int BUFFER_SIZE = 1024 * 1024; // 1 MB private bool _disposed = false; public MyClass() < _bufferPtr = Marshal.AllocHGlobal(BUFFER_SIZE); >protected virtual void Dispose(bool disposing) < if (_disposed) return; if (disposing) < // Free any other managed objects here. >// Free any unmanaged objects here. Marshal.FreeHGlobal(_bufferPtr); _disposed = true; > public void Dispose() < Dispose(true); GC.SuppressFinalize(this); >~MyClass() < Dispose(false); >>
Смысл этого шаблона — разрешить явное удаление ресурсов. А также чтобы добавить гарантии того, что ваши ресурсы будут удалены во время сборки мусора (в Finalizer ), если Dispose() не был вызван.
GC.SuppressFinalize(this) также имеет важное значение. Она гарантирует, что Finalizer не будет вызван при сборке мусора, если объект уже был удален. Объекты с Finalizer-ами освобождаются иначе и намного дороже. Finalizer добавляется к F-Reachable-Queue , которая позволяет объекту пережить дополнительную генерацию сборщика мусора. Есть и другие сложности.
7. Добавление телеметрии памяти из кода
Иногда вам может понадобиться периодически регистрировать использование памяти. Возможно, вы подозреваете, что на вашем рабочем сервере есть утечка памяти. Возможно, вы захотите предпринять какие-то действия, когда ваша память достигнет определенного предела. Или, может быть, у вас просто есть хорошая привычка следить за своей памятью.
Из самого приложения мы можем получить много информации. Получить текущую используемую память очень просто:
Process currentProc = Process.GetCurrentProcess(); var bytesInUse = currentProc.PrivateMemorySize64;
Для получения дополнительной информации вы можете использовать PerformanceCounter — класс, который используется для PerfMon :
PerformanceCounter ctr1 = new PerformanceCounter("Process", "Private Bytes", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr2 = new PerformanceCounter(".NET CLR Memory", "# Gen 0 Collections", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr3 = new PerformanceCounter(".NET CLR Memory", "# Gen 1 Collections", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr4 = new PerformanceCounter(".NET CLR Memory", "# Gen 2 Collections", Process.GetCurrentProcess().ProcessName); PerformanceCounter ctr5 = new PerformanceCounter(".NET CLR Memory", "Gen 0 heap size", Process.GetCurrentProcess().ProcessName); //. Debug.WriteLine("ctr1 = " + ctr1 .NextValue()); Debug.WriteLine("ctr2 = " + ctr2 .NextValue()); Debug.WriteLine("ctr3 = " + ctr3 .NextValue()); Debug.WriteLine("ctr4 = " + ctr4 .NextValue()); Debug.WriteLine("ctr5 table">
Доступна информация с любого счетчика perfMon, чего нам хватит с головой.
Однако вы можете пойти еще дальше. CLR MD (Microsoft.Diagnostics.Runtime) позволяет проверить текущую кучу и получить любую возможную информацию. Например, вы можете вывести все выделенные типы в памяти, включая количество экземпляров, пути к корням и так далее. Вы в значительной степени реализовали профилировщик памяти из кода.
Чтобы получить представление о том, чего можно достичь с помощью CLR MD, ознакомьтесь с DumpMiner Дуди Келети.
Вся эта информация может быть записана в файл или, что еще лучше, в инструмент телеметрии, такой как Application Insights.
8. Тестирование на утечки памяти
Профилактическое тестирование на утечки памяти — незаменимая практика. И это не так уж и сложно. Вот небольшой шаблон, который вы можете использовать:
[Test] void MemoryLeakTest() < var weakRef = new WeakReference(leakyObject) // Ryn an operation with leakyObject GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Assert.IsFalse(weakRef.IsAlive); >
Для более глубокого тестирования профилировщики памяти, такие как .NET Memory Profiler от SciTech и dotMemory, предоставляют тестовый API:
MemAssertion.NoInstances(typeof(MyLeakyClass)); MemAssertion.NoNewInstances(typeof(MyLeakyClass), lastSnapshot); MemAssertion.MaxNewInstances(typeof(Bitmap), 10);
Заключение
Не знаю, как у вас, но моя цель, поставленная на новый год, такова: лучшее управление памятью.
Я надеюсь, что эта статья принесет вам пользу и я буду рад, если вы подпишетесь на мой блог или оставите комментарий ниже. Любые отзывы приветствуются.
Как обнаружить утечку памяти
При разработке больших приложений, оперирующих большими объемами информации на первое место при отладке встает проблема обнаружения неправильного распределения памяти. Суть проблемы состоит в том, что если мы выделили участок памяти, а затем освободили не весь выделенный объем, то образуются блоки памяти, которые помечены как занятые, но на самом деле они не используются. При длительной работе программы такие блоки могут накапливаться, приводя к значительному расходу памяти.
Как обнаружить утечку памяти
Введение
При разработке больших приложений, оперирующих большими объемами информации на первое место при отладке встает проблема обнаружения неправильного распределения памяти. Суть проблемы состоит в том, что если мы выделили участок памяти, а затем освободили не весь выделенный объем, то образуются блоки памяти, которые помечены как занятые, но на самом деле они не используются. При длительной работе программы такие блоки могут накапливаться, приводя к значительному расходу памяти.
Для обнаружения подобных ошибок создано специализированное программное обеспечение (типа BoundsChecker от Numega), однако чаще бывает удобнее встроить механизм обнаружения утечки в свои проекты. Поэтому метод должен быть простым, и в то же время как можно более универсальным. Кроме того, не хотелось бы переписывать годами накопленные мегабайты кода, написанного и отлаженного задолго до того, как вам пришло в голову оградить себя от ошибок. Так что к списку требований добавляется стандартизация, т.е. нужно каким-то образом встроить защиту от ошибок в стандартный код.
Предлагаемое решение основывается на перегрузке стандартных операторов распределения памяти new и delete. Причем перегружать мы будем глобальные операторы new|delete, т.к. переписать эти операторы для каждого разработанного ранее класса было бы очень трудоемким процессом. Т.о. после перегрузки нам нужно будет только отследить распределение памяти и, соответственно, освобождение ее в момент завершения программы. Все несоответствия - ошибка.
Реализация
Проект написан на Visual C++, но переписать его на любой другой диалект С++ не будет слишком сложной задачей. Во-первых, нужно переопределить стандартные операторы new и delete так, чтобы это работало во всех проектах. Поэтому в stdafx.h добавляем следующий фрагмент:
#ifdef _DEBUG inline void * __cdecl operator new(unsigned int size, const char *file, int line) < >; inline void __cdecl operator delete(void *p) < >; #endif
Как видите, переопределение операторов происходит в блоке #ifdef/#endif. Это ограждает наш код от влияния на релиз компилируемой программы. Вы, наверное, заметили, что теперь оператор new имеет три параметра вместо одного. Два дополнительных параметра содержат имя файла и номер строки, в которой выделяется память. Это удобно для обнаружения конкретного места, где происходит ошибка. Однако код наших проектов по-прежнему ссылается на оператор new, принимающий один параметр. Для исправления этого несоответствия нужно добавиить следующий фрагмент
#ifdef _DEBUG #define DEBUG_NEW new(__FILE__, __LINE__) #else #define DEBUG_NEW new #endif #define new DEBUG_NEW
Теперь все наши операторы new будут вызываться с тремя параметрами, причем недостающие параметры подставит препроцессор. Конечно, пустые переопределенные функции ни в чем нам не помогут, так что давайте добавим в них какой-нибудь код:
#ifdef _DEBUG inline void * __cdecl operator new(unsigned int size, const char *file, int line) < void *ptr = (void *)malloc(size); AddTrack((DWORD)ptr, size, file, line); return(ptr); >; inline void __cdecl operator delete(void *p) < RemoveTrack((DWORD)p); free(p); >; #endif
Для полноты картины нужно переопределить операторы new[] и delete[], однако никаких существенных отличий здесь нет - творите!
Последний штрих - пишем функции AddTrack() и RemoveTrack(). Для создания списка используемых блоков памяти будем использовать стандартные средства STL:
typedef struct < DWORD address; DWORD size; char file[64]; DWORD line; >ALLOC_INFO; typedef list AllocList; AllocList *allocList; void AddTrack(DWORD addr, DWORD asize, const char *fname, DWORD lnum) < ALLOC_INFO *info; if(!allocList) < allocList = new(AllocList); >info = new(ALLOC_INFO); info->address = addr; strncpy(info->file, fname, 63); info->line = lnum; info->size = asize; allocList->insert(allocList->begin(), info); >; void RemoveTrack(DWORD addr) < AllocList::iterator i; if(!allocList) return; for(i = allocList->begin(); i != allocList->end(); i++) < if((*i)->address == addr) < allocList->remove((*i)); break; > > >;
Перед самым завершением программы наш список allocList содержит ссылки на блоки памяти, котороые не были освобождены. Все, что нужно сделать - вывести эту информацию куда-нибудь. В нашем проекте мы выведем список неосвобожденных участков памяти в окно вывода отладочных сообщений Visual C++:
void DumpUnfreed() < AllocList::iterator i; DWORD totalSize = 0; char buf[1024]; if(!allocList) return; for(i = allocList->begin(); i != allocList->end(); i++) < sprintf(buf, "%-50s:\tLINE %d,\tADDRESS %d %d unfreed", (*i)->file, (*i)->line, (*i)->address, (*i)->size); OutputDebugString(buf); totalSize += (*i)->size; > sprintf(buf, "--------------------------------------------------"); OutputDebugString(buf); sprintf(buf, "Total Unfreed: %d bytes", totalSize); OutputDebugString(buf); >;
Надеюсь, этот проект сделает ваши баг-листы короче, а программы устойчивее. Удачи!
Что такое утечки памяти в android, как проверить программу на их отсутствие и как предотвратить их появление
В этой статье для начинающих android-разработчиков я постараюсь рассказать о том, что такое «утечки памяти» в android, почему о них стоит думать на современных устройствах, выделяющих по 192МБ на приложение, как быстро найти и устранить эти утечки в малознакомом приложении и на что нужно обращать особое внимание при разработке любого приложения.

Конечная цель этой статьи — ответ на простой вопрос:
Куда нажать, чтобы узнать, какую строчку в приложении поправить?
Что такое «утечка памяти»?
Начнем с того, что же называется «утечкой памяти». В строгом понимании объект можно назвать утечкой памяти, если он продолжает существовать в памяти даже после того, как на него потеряны все ссылки. С этим определением сразу же возникает проблема: память для всех объектов, которые вы создаете, выделяется при участии сборщика мусора, и все созданные объекты сборщик мусора помнит, независимо от того, есть у вас ссылка на объект, или нет.
На самом деле сборщик мусора устроен крайне примитивно (на самом деле нет — но принцип работы действительно простой): есть граф, в котором каждый существующий объект — это вершина, а ссылка от любого объекта на любой другой объект — ребро. Некоторые вершины на этом графе — особые. Это корни сборщика мусора (garbage collection roots) — те сущности, которые созданы системой и продолжают свое существование независимо от того, ссылаются на них другие объекты или нет. Если и только если на графе существует любой путь от данного объекта до любого корня, объект не будет уничтожен сборщиком мусора.
В этом и заключается проблема — если объект не уничтожен, значит существует цепочка ссылок от корня до данного объекта (либо, если такой цепочки не существует, объект будет уничтожен при следующей сборке мусора).А это значит, что ни один объект не может являться утечкой памяти в строгом понимании этого термина. Собственно даже того, что сам сборщик мусора хранит ссылку на каждый существующий объект в системе, уже достаточно.
Попытки получить в java «чистую» утечку памяти предпринимались неоднократно и, безусловно, продолжают предприниматься, однако ни один из способов не способен заставить сборщика мусора забыть ссылку на объект, не освободив память. Существуют утечки памяти, связанные с выделением памяти нативным кодом (JNI), однако в этой статье мы их не будем рассматривать.
Вывод: вы можете потерять все ссылки на интересующий вас объект, но сборщик мусора помнит.
Итак, определение «утечки памяти» в строгом смысле нам не подходит. Поэтому далее будем понимать утечку памяти как объект, который продолжает существовать после того, как он должен быть уничтожен.
Далее я покажу несколько наиболее распространенных случаев утечек памяти, покажу как их обнаруживать и как избегать. Если вы научитесь устранять эти типовые утечки памяти, то с вероятностью 99.9%, в вашем приложении не будет утечек памяти, о которых стоит волноваться.
Но, прежде чем перейти к описанию этих частых ошибок, нужно ответить на главный вопрос: а нужно ли вообще исправлять эти ошибки? Приложение-то работает.
Почему нужно тратить время на устранение утечек памяти?

Приложения уже давно не падают из-за того, что вы забыли пережать ресурсы в папку drawable-ldpi. Готовясь к написанию этой статьи, я провел простой эксперимент: я взял одно из работающих приложений, и добавил в него утечку памяти таким образом, что ни одно создаваемое activity никогда не выгружалось из памяти (стал добавлять их в статический список). Я открыл приложение и начал прокликивать экраны, ожидая, когда же приложение наконец упадет на моем Nexus 5. Наконец, через 5 минут и 55 экранов, приложение упало. Ирония в том, что, по данным Google Analytics, обычно пользователь за сессию посещает 3 экрана.
Так нужно ли волноваться по поводу утечек памяти, если пользователь может их просто не заметить? Да, и есть три причины почему.
Во-первых, если в вашем приложении работает что-то, что работать не должно, это может привести к очень серьёзным и трудно отлаживаемым проблемам.
Например, вы разработали приложение для социальной сети. В этом приложении можно обмениваться сообщениями между пользователями, где на экране обмена сообщениями есть таймер, который делает запрос на сервер каждые 10 секунд с целью получения новых сообщений, но вы забыли этот таймер выключить при выходе с экрана. К чему это приведет визуально? Да ни к чему. Вы не заметите, что приложение делает что-то не то. Но при этом приложение продолжит каждые 10 секунд посылать запрос на сервер. Даже после того, как вы выйдете из приложения. Даже после того, как вы выключите экран (поведение может варьироваться от телефона). Если пользователь зайдет на экраны общения с тремя разными друзьями, в течение часа вы получите 1000 лишних запросов на сервер и одного пользователя, очень рассерженного на ваше приложение, которое усиленно потребляет батарею. Именно такие результаты я получил с тестовым приложением на телефоне с выключенным экраном.
Вы можете возразить, что это не утечка памяти, а всего лишь не выключенный таймер и это совсем другая ошибка. Это неважно. Важно, что проверив свое приложение на наличие утечек памяти, вы найдете и другие ошибки. Когда мы проверяем приложение на наличие утечек памяти, мы хотим найти все объекты, которые существуют, но существовать не должны. Находя такие объекты, мы сразу понимаем, какие лишние операции продолжают выполняться.
Во-вторых, не все приложения потребляют мало памяти, и не все телефоны выделяют много памяти.
Помните про приложение, которое упало только после 5 минут и 55 не выгруженных экранов? Так вот для этого же приложения мне каждую неделю приходит 1-2 отчета о падении с OutOfMemoryException (в основном с устройств до 4.0; у приложения 50.000 установок). И это при том, что утечек памяти в приложении нет. Поэтому даже сейчас вы можете изрядно подпортить себе карму, выложив приложение с утечками памяти, особенно если ваше приложение потребляет много памяти. Как обычно в мире android, от блестящего будущего нас отделяет суровое настоящее.
В-третьих, мужик должен всё уметь! (я же обещал, что все 3 причины будут серьёзные)
Теперь, когда я, надеюсь, убедил вас в необходимости отлавливать утечки памяти, давайте рассмотрим основные причины их появления.
Никогда не сохраняйте ссылки на activity (view, fragment, service) в статических переменных
Один из первых вопросов, с которым сталкивается каждый начинающий разработчик, это как передать объект из одного activity в следующий. Самое простое и самое неправильное решение, которое мне периодически приходится видеть, это запись первого activity в статическую переменную и обращение к этой переменной из второго activity. Это крайне неудачный подход. Не только потому, что он моментально приводит к утечке памяти (статическая переменная продолжит существовать пока существует приложение, и activity, на который она ссылается, никогда не будет выгружен). Этот подход также может привести к ситуации, когда вы будете обмениваться информацией не с тем экраном, ведь экран, невидимый пользователю, может в любой момент быть уничтожен и пересоздан лишь когда пользователь к нему вернется.
Почему же утечка activity — такая большая проблема? Дело в том, что если сборщик мусора не соберет activity, то он не соберет и все view и fragment, а вместе с ними и все прочие объекты, расположенные на activity. В том числе не будут высвобождены картинки. Поэтому утечка любого activity — это, как правило, самая большая утечка памяти, которая может быть в вашем приложении.
Никогда не записывайте ссылки на activity в статические переменные. Используйте передачу объектов через Intent, либо вообще передавайте не объект, а id объекта (если у вас есть база данных, из которой этот id потом можно достать).
Этот пункт также относится к любым объектам, временем жизни которых напрямую или косвенно управляет android. Т.е. к view, fragment, service и т.д..
View и fragment объекты содержат ссылку на activity, в котором они расположены, поэтому, если утечет один единственный view, утечет сразу всё — activity и все view в нём, а, вместе с ними, и все drawable и всё, на что у любого элемента из экрана есть ссылка!
Будьте аккуратны при передаче ссылки на activity (view, fragment, service) в другие объекты
Рассмотрим простой пример: ваше приложение для социальной сети отображает фамилию, имя и рейтинг текущего пользователя на каждом экране приложения. Объект с профилем текущего пользователя существует с момента входа в аккаунт до момента выхода из него, и все экраны вашего приложения обращаются за информацией к одному и тому же объекту. Этот объект также периодически обновляет данные с сервера, так как рейтинг может часто меняться. Необходимо, чтобы объект с профилем уведомлял текущее activity об обновлении рейтинга. Как этого добиться? Очень просто:
@Override protected void onResume()
Как добиться в этой ситуации утечки памяти? Тоже очень несложно! Просто забудьте отписаться от уведомлений в методе onPause:
@Override protected void onPause() < super.onPause(); /* Забудьте про следующую строчку и вы получите серьёзную утечку памяти */ currentUser.removeOnUserUpdateListener(this); >
Из-за такой утечки памяти activity будет продолжать обновлять интерфейс каждый раз, когда профиль будет обновляться даже после того, как экран перестанет быть видим пользователю. Хуже того, таким образом экран может подписать 2, 3 или больше раза на одно и то же уведомление. Это может привести к видимым тормозам интерфейса в момент обновления профиля — и не только на этом экране.
Что делать, чтобы избежать этой ошибки?
Во-первых, конечно нужно всегда внимательно следить за тем, что вы отписались от всех уведомлений в момент ухода activity в фон.
Во-вторых, вы должны периодически проверять своё приложение на наличие утечек памяти.
В-третьих, есть и альтернативный подход к проблеме: вы можете сохранять не ссылки на объекты, а слабые ссылки. Это особенно полезно для наследников класса View — ведь у них нет метода onPause и не совсем понятно, в какой момент они должны отписываться от уведомления. Слабые ссылки не считаются сборщиком мусора как связи между объектами, поэтому объект, на который существуют только слабые ссылки, будет уничтожен, а ссылка перестанет ссылаться на объект и примет значение null. Чтобы не возиться каждый раз с не очень удобными в использовании слабыми ссылками, вы можете воспользоваться примерно следующим шаблонным классом:
public class Observer < private ArrayListstrongListeners = new ArrayList(); private ArrayList weakListeners = new ArrayList(); public void addStrongListener(I listener) < strongListeners.add(listener); >public void addWeakListener(I listener) < weakListeners.add(new WeakReference(listener)); > public void removeListener(I listener) < strongListeners.remove(listener); for (int i = 0; i < weakListeners.size(); ++i) < WeakReferenceref = weakListeners.get(i); if (ref.get() == null || ref.get() == listener) < weakListeners.remove(i--); >> > public List getListeners() < ArrayListactiveListeners = new ArrayList(); activeListeners.addAll(strongListeners); for (int i = 0; i < weakListeners.size(); ++i) < WeakReferenceref = weakListeners.get(i); I listener = ref.get(); if (listener == null) < weakListeners.remove(i--); continue; >activeListeners.add(listener); > return activeListeners; > >
Который будет работать примерно вот так:
public class User < . public interface OnUserUpdateListener < public void onUserUpdate(); >private Observer updateObserver = new Observer(); public Observer getUpdateObserver() < return updateObserver; >> . @Override protected void onFinishInflate() < super.onFinishInflate(); /* Мы подписываемся на уведомления при создании объекта */ currentUser.getUpdateObserver().addWeakListener(this); >/* . и никогда от этих уведомлений не отписываемся */ .
Да, вы можете получить лишние обновления этого view. Но часто это — меньшее из зол. И, при любом раскладе, утечку памяти вы уже не получите.
Есть только одна тонкость при использовании метода addWeakListener: на объект, который вы добавляете, должен кто-то ссылаться. Иначе сборщик мусора уничтожит этот объект до того, как он получит свое первое уведомление:
/* Не делайте так! */ currentUser.getUpdateObserver().addWeakListener(new OnUserUpdateListener() < @Override public void onUserUpdate() < /* Этот код не будет вызван */ >>);
Таймеры и потоки, которые не отменяются при выходе с экрана
Про эту проблему я уже рассказывал выше: итак, вы разработали приложение для социальной сети. В этом приложении можно обмениваться сообщениями между пользователями, и вы добавляете на экран обмена сообщениями таймер, который делает запрос на сервер каждые 10 секунд с целью получения новых сообщений, но вы забыли этот таймер выключить при выходе с экрана:
public class HandlerActivity extends Activity < private Handler mainLoopHandler = new Handler(Looper.getMainLooper()); private Runnable queryServerRunnable = new Runnable() < @Override public void run() < new QueryServerTask().execute(); mainLoopHandler.postDelayed(queryServerRunnable, 10000); >>; @Override protected void onResume() < super.onResume(); mainLoopHandler.post(queryServerRunnable); >@Override protected void onPause() < super.onPause(); /* Вы забыли написать строчку ниже и в вашем приложении появилась утечка памяти */ /* mainLoopHandler.removeCallbacks(queryServerRunnable); */ >. >
К сожалению, эту проблему сложно избежать. Единственные два совета, которые можно дать, будут такими же, как и в предыдущем пункте: будьте внимательны и периодически проверяйте приложение на утечки памяти. Вы также можете использовать аналогичный предыдущему пункту подход с использованием слабых ссылок.
Никогда не сохраняйте ссылки на fragment в activity или другом fragment
Я очень много раз видел эту ошибку. Activity хранит ссылки на 5-6 запущенных фрагментов даже не смотря на то, что на экране всегда виден только 1. Один фрагмент хранит ссылку на другой фрагмент. Фрагменты, видимые на экране в разное время, общаются друг с другом по прямым закешированным ссылкам. FragmentManager в таких приложениях выполняет чаще всего рудиментарную роль — в нужный момент он подменяет содержимое контейнера нужным фрагментом, а сами фрагменты в back stack не добавляются (добавление фрагмента, на который у вас есть прямая ссылка, в back stack рано или поздно приведет к тому, что фрагмент будет выгружен из памяти; после возврата к этому фрагменту будет создан новый, а ваша ссылка продолжит ссылаться на существующий, но невидимый пользователю фрагмент).
Это очень плохой подход по целому ряду причин.
Во-первых, если вы храните в activity прямые ссылки на 5-6 фрагментов, то это тоже самое, как если бы вы хранили ссылки на 5-6 activity. Весь интерфейс, все картинки и вся логика 5 неиспользуемых фрагментов не могут быть выгружены из памяти, пока запущено activity.
Во-вторых, эти фрагменты становится крайне сложно переиспользовать. Попробуйте перенести фрагмент в другое место программы при условии, что он должен быть обязательно запущен в одном activity с фрагментами, x, y и z, которые переносить не надо.
Относитесь к фрагментам как к activity. Делайте их максимально модульными, общайтесь между фрагментами только через activity и fragmentManager. Это может казаться излишне сложной системой: зачем так стараться, когда можно просто передать ссылку? Но, на самом деле, такой подход сделает вашу программу лучше и проще.
По этой теме есть отличная официальная статья от Google: «Communicating with Other Fragments». Перечитайте эту статью и никогда больше не сохраняйте указатели на фрагменты.
Обобщённое правило
После прочтения четырех предыдущих пунктов вы могли заметить, что они практически ничем не отличаются. Все это — частные случаи одного общего правила.
Все утечки памяти появляются тогда и только тогда, когда вы сохраняете ссылку на объект с коротким жизненным циклом (short-lived object) в объекте с длинным жизненным циклом (long-lived object).
Помните об этом и всегда внимательно относитесь к таким ситуациям.
У этого правила нет красивого короткого названия, такого как KISS, YAGNI или RTFM, но оно применимо ко всем языкам со сборщиком мусора и ко всем объектам, а не только к activity в android.
Теперь, когда я, надеюсь, показал основные источники утечек памяти, давайте наконец перейдём к их выявлению в рабочем приложении.
Куда нажать, чтобы узнать, какую строчку в приложении поправить?
Итак, вы знаете как избежать утечек памяти, но это не ограждает вас от опечаток, багов и проектов, которые вы написали до того, как узнали, как избежать утечек памяти.
Для того, чтобы определить наличие и источник утечек памяти в приложении вам потребуется немного времени и MAT. Если вы никогда раньше не пользовались MAT, установите его как plugin к eclipse, откройте DDMS perspective и найдите кнопку «Dump HPROF file». Нажатие на эту кнопку откроет дамп памяти выбранного приложения. Если вы используете Android Studio, то процесс будет немного сложнее, так как на данный момент MAT все ещё не существует как плагин к Android Studio. Поставьте MAT как отдельную программу и воспользуйтесь инструкцией со stackoverflow.
Выполните следующие шаги:
- Установите приложение на устройство, подключенное к компьютеру и попользуйтесь им таким образом, чтобы оказаться на каждом экране как минимум однажды. Если один экран может быть открыт с разными параметрами, постарайтесь открыть его со всеми возможными комбинациями параметров. Вообщем — пройдитесь по всему приложению, как если бы вы проверяли его перед релизом. После того как вы прошли все экраны, нажимайте кнопку «назад» до тех пор, пока не выйдите из приложения. Не нажимайте кнопку home — ваша задача завершить все запущенные activity, а не просто скрыть их.
- Нажмите на кнопку Cause GC несколько раз. Если вы этого не сделаете, в дампе будут видны объекты, которые подлежат уничтожению сборщиком мусора, но ещё не были уничтожены.
- Сделайте дамп памяти приложения нажав на кнопку «Dump HPROF file».
- В открывшемся окне сделайте OQL запрос: «SELECT * FROM instanceof android.app.Activity»



Заключение
Если вы прокликали все экраны в своем приложении и не нашли ни одного подозрительного объекта, то, с вероятностью 99.9%, в вашем приложении нет серьёзных утечек памяти.
Этих проверок действительно достаточно практически для любого приложения. Вас должны интересовать только утечки памяти, действительно способные повлиять на работу приложения. Утечка объекта, содержащего строковый uuid и пару коротких строк — это ошибка, на исправление которой просто не стоит тратить свое время.
Список литературы
- Investigating Your RAM Usage
https://developer.android.com/tools/debugging/debugging-memory.html
- Java Performance blog
http://kohlerm.blogspot.ru/2009/07/eclipse-memory-analyzer-10-useful.html
- Avoiding memory leaks
http://android-developers.blogspot.co.uk/2009/01/avoiding-memory-leaks.html
- Memory Analysis for Android Applications
http://android-developers.blogspot.ru/2011/03/memory-analysis-for-android.html
- Detecting a Memory Leak
http://blog.crowdint.com/2013/10/02/fixing-memory-leaks-in-android-applications.html
- DEBUGGING MEMORY LEAKS ON ANDROID FOR BEGINNERS: PROGRAMMATIC HEAP DUMPING
http://novoda.com/blog/memory-debugging-on-android-part-1
- How To Identify If Your App is Leaking Memory
http://www.littleeye.co/blog/2013/04/24/identify-memory-leaks-android-apps/
- Fixing an Android Memory Leak
http://therockncoder.blogspot.ru/2012/09/fixing-android-memory-leak.html
- Managing Your App's Memory
https://developer.android.com/training/articles/memory.html
- HUNTING YOUR LEAKS: MEMORY MANAGEMENT IN ANDROID
http://www.raizlabs.com/dev/2014/03/wrangling-dalvik-memory-management-in-android-part-1-of-2/
http://www.raizlabs.com/dev/2014/04/hunting-your-leaks-memory-management-in-android-part-2-of-2/
- How to discover memory usage of my application in Android
http://stackoverflow.com/questions/2298208/how-to-discover-memory-usage-of-my-application-in-android/2299813#2299813
- Android
- memory management
- память
- утечки памяти
- руководство для новичков