От переводчика: Представляю вашему вниманию перевод статьи 2019 года. И хоть с момента публикации прошло уже больше двух лет, статья более чем актуальна и врядли утратит свое значение в ближайшие пару лет. Опытные .NET-разработчики знают, что даже несмотря на наличие в .NET сборщика мусора (Garbage Collector, GC), утечки памяти все равно возникают с завидной регулярностью. Утечки возможны не из-за ошибок в сборщике мусора, а потому что даже в управляемом коде есть множество способов их появления. Утечки памяти — довольно коварные сущности. Их можно долго не замечать, пока они медленно убивают приложение. При этом растет потребление памяти, создавая нагрузку на сборщик мусора и проблемы с производительностью. В конце концов приложение просто падает с исключением Out of memory. В этой статье мы пройдемся по наиболее частым причинам возникновения утечек памяти в .NET-приложениях. Все примеры написаны на C#, но описанные проблемы и способы их решения справедливы и для других .NET-языков. Термин утечка памяти в средах со сборщиком мусора может вызывать некоторое недоумение. В конце концов, как может утекать память, если есть сборщик мусора, который следит за её своевременным освобождением?  На это есть 2 основные причины. Первая — это объекты, которые не используются в программе, но на которые еще сохранились ссылки. Из-за того, что в других участках кода на объекты есть ссылки, сборщик мусора не освобождает занятую ими память, так что они сохраняются навсегда, удерживая выделенную под них память. Так происходит, например, когда вы регистрируете обработчик события, но не удаляете его. Назовем такие утечки утечками управляемой памяти. Вторая причина — неаккуратная работа с неуправляемой памятью, когда вы каким-либо способом выделяете неуправляемую память, но не освобождаете ее. На самом деле, это не так уж и сложно сделать в управляемом, даже работая с управляемым кодом. Сам .NET имеет множество классов, которые выделяют неуправляемую память. Почти всё, что использует потоки, графику, файловую систему или сетевые вызовы, под капотом работает с неуправляемой памятью. Вы можете легко выделить неуправляемую память и самостоятельно, например при помощи специальных классов (таких как Marshal) или при помощи P/Invoke.  Многие разделяют мнение, что утечки управляемой памяти — это вовсе не утечки, ведь на них все еще есть ссылки и в теории, память все еще можно освободить. Это дискуссионный вопрос, но на мой взгляд, это все же утечки памяти. Они удерживают память, которая не может быть выделена другому экземпляру и в конечном итоге вызывают исключение Out of Memory. В этой статье я буду называть утечки и управляемой, и неуправляемой памяти просто утечками памяти. Ниже приведено 8 наиболее часто встречающихся причин возникновения утечек. Первые 6 касаются утечек управляемой памяти, оставшиеся 2 — неуправляемой. События в .NET печально известны утечками памяти. Причина проста: после подписки на событие какого-либо объекта, он будет удерживать ссылку на ваш класс, в котором вы определили обработчик (если, конечно, вы не использовали в качестве обработчика анонимный метод, не захватывающий членов класса). Посмотрите на этот пример: Так, если wifiManager определен за пределами MyClass, то мы получили утечку памяти. wifiManager ссылается на экземпляр MyClass, который теперь никогда не будет удален сборщиком мусора. События действительно очень опасны, и про это есть отдельная статья: 5 Техник избежать утечек памяти при использовании событий в C# .NET, о которых вам нужно знать. Что можно сделать в этой ситуации? В вышеуказанной статье описано несколько хороших практик, позволяющих избежать утечек памяти. Не вдаваясь в подробности, вот некоторые из них: Всегда отписывайтесь от событий. Используйте паттерны слабых событий (Weak Event Pattern). Если это возможно, подписывайтесь на события при помощи анонимных методов, не захватывающих других членов класса. Довольно очевидно, что использование метода в качестве обработчика события приведет к созданию в обработчике ссылки на объект, содержащий этот метод. Но куда менее очевидно, что то же самое происходит, когда член класса захвачен в анонимном методе. Вот пример: В этом примере член класса _id захвачен в анонимном методе и, как результат, экземпляр класса хранит ссылку на себя. Это означает, что пока _jobQueue существует и ссылается на анонимный делегат, он [_jobQueue] ссылается также и на экземпляр MyClass. Решение проблемы здесь простое — использовать локальную переменную: Если присвоить значение локальной переменной, член класса не будет захвачен и вы предотвратите утечку памяти. Примечание переводчика: если вам не совсем понятна природа возникновения утечки в данном случае, обратите внимание на этот комментарий. Некоторые разработчики считают, что использование статических переменных являются плохой практикой. Тем не менее, говоря об утечках памяти, о них нельзя не упомянуть. Прежде, чем подойти к сути этого раздела, давайте немного поговорим о работе сборщика мусора в .NET. Основная идея состоит в том, что сборщик мусора проходит по всем корневым объектам (GC Roots, корни) и помечает их, как объекты, которые не будут для очищены при сборке. Затем сборщик мусора проходит по всем объектам, на которые ссылаются корни, и точно также помечает их. И так далее. В конце концов, сборщик мусора собирает всё оставшееся (отличная статья о сборщике мусора). Что считается корневыми объектами? Cтек исполняющихся потоков. Статические переменные. Управляемые объекты, переданные COM-объектам через Interop. Это означает, что статические переменные и всё, на что они ссылаются, никогда не будет освобождено сборщиком мусора. Вот пример: Если вы зачем-то напишете вышеприведенный код, любой экземпляр MyClass навсегда останется в памяти, тем самым вызвав утечку. Разработчики любят кэширование. Действительно, зачем выполнять операцию дважды, если можно выполнить ее один раз и сохранить результат, не так ли? Это правда, но если кэшировать бесконечно, то в конце концов вы исчерпаете всю доступную память. Посмотрите на этот пример: Кэширование в этом примере помогает сократить дорогостоящие операции обращения к базе данных, но ценой является захламление памяти. Для решения проблемы можно использовать следующие практики: Удалять из кэша данные, которые не используются какое-то время. Ограничить размер кэша. Использовать WeakReference для хранения кэшируемых объектов. WeakReference сборщику мусора самостоятельно очищать кэш, что в ряде случаев может оказаться не такой уж и плохой идеей. Сборщик мусора будет перемещать объекты, которые еще используются, в старшие поколения, чтобы держать их в памяти дольше. Это означает, что часто используемые объекты останутся в кэше дольше, тогда как неиспользуемые будут удалены сборщиком мусора без вашего явного участия. Привязка данных (Data Binding) в WPF тоже может стать причиной утечек памяти. Главное правило для предотвращения утечек — всегда использовать DependencyObject или INotifyPropertyChanged. Если вы этого не делаете, WPF создает т.н. сильную ссылку (strong reference) на объект, вызывая утечку памяти (более подробное объяснение). Прммер: Представленный ниже класс останется в памяти навсегда: А вот этот класс уже не вызовет утечки: На самом деле даже не важно, вызываете вы PropertyChanged или нет, главное, что класс реализует интерфейс INotifyPropertyChanged. Это говорит инфраструктуре WPF не создавать сильную ссылку.  Утечки памяти возникают только если используется режим привязки OneWay или TwoWay. Если привязка осуществляется в режиме OneTime или OneWayToSource, то проблемы не будет.  Утечки памяти в WPF также могут возникать, когда происходит привязка коллекций. Если коллекция не реализует INotifyCollectionChanged, вы получите утечку памяти. Вы можете избежать проблемы используя класс ObservableCollection, который этот интерфейс реализует. Мы уже говорили о том, как работает сборщик мусора и о корневых объектах. Я упоминал, что стек потока считается корневым объектом. Стек потока включает все локальные переменные, а также члены стеков вызовов. Если вы зачем-то создали бесконечный поток, который ничего не делает и ссылается на объекты, то возникнет утечка памяти. Один из примеров того, как это может легко случиться — неправильное использование класса Timer. Посмотрите на этот код:  Если вы не остановите таймер, он будет бесконечно выполняться в отдельном потоке, удерживая ссылку на MyClass и предотвращая его удаление сборщиком мусора. До сих пор мы говорили только об управляемой памяти, которая освобождается сборщиком мусора. Совсем другое дело — неуправляемая память. Вместо того, чтобы просто избегать ссылок на ненужные объекты, в этом случае вам необходимо явно освобождать память. Вот простой пример: В этом примере мы использовали Marshal.AllocHGlobal, чтобы выделить участок неуправляемой памяти (см. документацию в MSDN). Если явно не освободить память при помощи Marshal.FreeHGlobal, она будет считаться выделенной в куче процесса, вызывая утечку памяти, даже после удаления SomeClass сборщиком мусора. Для предотвращения подобных проблем вы можете добавить в свой класс метод Dispose, в котором очищать неуправляемые ресурсы. Например: Утечки неуправляемой памяти даже хуже, чем утечки управляемой памяти в связи с фрагментацией. Сборщик мусора умеет дефрагментировать управляемую память, помещая неудаленные объекты рядом, чтобы освободить место для новых данных. В свою очередь, неуправляемая память навсегда привязывается к месту, в котором она выделена. В последнем примере мы добавили метод Dispose для освобождения неуправляемых ресурсов, когда они больше не нужны. Это прекрасно, но что случится, если кто-нибудь использует класс, но не вызовет метод Dispose? Что вы можете сделать, так это использовать конструкцию using языка C#: Конструкция из примера работает на классах, реализующих интерфейс IDisposable и при компиляции автоматически преобразуется в следующий код: Это довольно удобно, потому что если будет выброшено исключение, метод Dispose все равно будет вызван. Для достижения наибольшей надежности MSDN предлагает паттерн реализации Dispose. Вот пример его использования: Использование этого паттерна позволяет гарантировать, что даже если метод Dispose не был вызван явно, то он все равно будет вызван финализатором, когда сборщик мусора решит удалить объект. Если же Dispose вызывался вручную, финализатор для объекта отключается и вызван не будет. Отмена финализатора очень важна, так как его вызов обходится достаточно дорого и может вызывать проблемы с производительностью. Но учтите, что серебряной пулей майкрософтовский паттерн Dispose не является. Если не вызвать Dispose вручную, и при этом объект не удален сборщиком мусора из-за утечки управляемой памяти, то и неуправляемые ресурсы освобождены не будут. Безусловно, разработчику очень важно понимать, как возникают утечки памяти, но это лишь часть общей картины. Не менее важно научиться распознавать, локализовывать и устранять утечки памяти в приложениях. Больше информации по теме вы можете найти в статье: Найти, исправить и избежать утечек памяти в C# .NET: 8 лучших практик. Надеюсь, что статья и перевод были для вас полезными. Удачного программирования.  Разработчик