February 18, 2010

Управляемая память тоже утекает

Предыстория

Однажды давным давно, кажется в прошлую пятницу, был написан некий код, в задачи которого входило агрегировать полученные, скажем, из сети данные. Данные эти имеют свойство приходить разной длины пакетиками, и код должен во-первых, сообщать о приходе каждого пакета, и во-вторых, по запросу отдавать всё в виде одного большого куска.

Код написан на C#: .net, сборка мусора, все дела... Никто не ожидал подвоха. И вот в один прекрасный момент в виде данных попробовали передавать кучу относительно небольших, размером ~200Кб, картинок. И тут всё сломалось... Куда-то стала пропадать свободная память. Открыли Process Explorer, посмотрели, удивились: растёт LOH. Название говорящее, как бы намекает нам о том, что мы были не правы, когда понадеялись на сулящий золотые горы механизм сборки мусора.

LOH

LOH - Large Object Heap, что в переводе на русский звучит как "боЛьших Объектов Хранилище", является тем самым участком памяти, в который попадают, как можно догадаться из названия, большие объекты. Достаточно большим объектом в .Net считается всё, что больше 85000 байт. К таким объектам не применимы общие правила работы с памятью, то есть они не проходят через поколения сборщика мусора, а при инициализации попадают прямо в ЛОХ, потому как полагается, что объекты такого размера с большой вероятностью могут ещё пригодиться, а проводить их через все стандартные процедуры - слишком дорогое занятие.

Что же случилось

Согласно всем рекомендациям ведущих дотнетоводов (и дотнетоводоведов) для операций со строками использовался класс StringBuilder, куда все пакеты добавлялись при помощи метода Append. Для проверки данных по мере их накопления время от времени вызывался StringBuilder.ToString, который, при передаче картинок создавал гигантского размера строку, которая в свою очередь помещалась в LOH, и никуда оттуда не исчезала, даже несмотря на отсутствие внешних ссылок. Согласно найденной документации, сборка мусора в Generation 2 и LOH происходит одновременно и только по одной из следующих причин: 1) размер занимаемой поколением памяти достиг определённого предела, 2) явный вызов GC.Collect, 3) системе категорически не хватает памяти. По всей видимости, ни одно из этих условий не было выполнено, поэтому картинки продолжали выедать системную память.

Как такое лечить

Во-первых, чтобы понять что конкретно происходит с памятью в .net приложении, можно использовать вышеназванный ProcessExplorer, который позволяет в свойствах процесса, на вкладке .NET выбрать набор счётчиков производительности ".NET CLR Memory" и отслеживать их значения в процессе работы приложения. Кроме того, можно воспользоваться стандартным интерфейсом просмотра счётчиков производительности - утилитой perfmon, которая, насколько мне известно, присутствует в любой современной версии Windows.

Во-вторых, чтобы однозначно определить, что же является тем ресурсом, который выедает всю память, можно воспользоваться замечательным набором Debugging Tools For Windows, который можно задаром получить с сайта Microsoft. Супер подробное описание способа работы с этим дебагером можно найти в блоге Tess Ferrandez. Здесь же я перечислю пару команд, которые помогли мне:
Этих 5 простых команд вполне достаточно, чтобы обнаружить виновников утечек памяти, а после их устранения проверить, что теперь-то всё работает как надо.

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

Заключение

Строки, как и StringBuilder не имеют механизма принудительной очистки занимаемой памяти, так что в следующий раз, когда понадобится разрабатывать что-то подобное, я скорее всего задумаюсь об использовании MemoryStream, или ещё чего подобного.

February 9, 2010

Прелюдия к компилятору

По причине сложности внедрения кучи блоков кода в пост, самое свежее своё творение решил оформить в виде wiki и сложить его прямо рядом с репозиторием кода на bitbucket. Так что, если кому интересно почитать про то, как можно конвертировать строковые where-выражения в .Net Expression Trees, и всё это на F# - языке, которого нет - добро пожаловать по адресу http://bitbucket.org/moiseev/wepr/wiki/Intro.

This page is powered by Blogger. Isn't yours?