Простой пример WPF вызывает неконтролируемый рост памяти

#c# #wpf #memory-leaks

Вопрос:

Я свел проблему, которую вижу в одном из своих приложений, к невероятно простому образцу воспроизведения. Мне нужно знать, если что-то не так или я что-то упускаю.

В любом случае, ниже приведен код. Поведение заключается в том, что код выполняется и неуклонно увеличивается в памяти, пока не произойдет сбой с исключением OutOfMemoryException. Это занимает некоторое время, но поведение таково, что объекты выделяются, а не собираются в мусор.

Я взял дампы памяти и запустил !gcroot на некоторых вещах, а также использовал МУРАВЬЕВ, чтобы выяснить, в чем проблема, но я занимался этим некоторое время и нуждаюсь в новых глазах.

Этот образец воспроизведения представляет собой простое консольное приложение, которое создает холст и добавляет к нему строку. Он делает это постоянно. Это все, что делает код. Он спит время от времени, чтобы убедиться, что процессор не так облагается налогом, что ваша система не отвечает (и чтобы убедиться, что нет никаких странностей в том, что GC не может работать).

У кого-нибудь есть какие-нибудь мысли? Я пробовал это только с .NET 3.0, .NET 3.5, а также .NET 3.5 SP1, и такое же поведение наблюдалось во всех трех средах.

Также обратите внимание, что я также поместил этот код в проект приложения WPF и запустил код нажатием кнопки, и это тоже происходит там.

использование Системы;
использование Системы.Коллекции.Общий;
использование System.Linq;
использование System.Text;
использование System.Windows.Управления;
используя Систему.Windows.Формы;
используя Систему.Windows;

простейший пример пространства имен
{
 программа занятий
 {
 [Прочитал]
 статическая пустота Main(строка[] args)
 {
 длительное количество = 0;
 в то время как (правда)
 {
если (количество   % 100 == 0)
 {
 // поспите некоторое время, чтобы убедиться, что мы не используем весь процессор
 Система.Нарезание резьбы.Нитки.Сон(50);
}
 BuildCanvas();
 }
 }

 [Система.Время выполнения.Услуги компиляторов.Методимпл(System.Runtime.Услуги компиляторов.Методимпопции.не наклоняясь)]
 частная статическая пустота BuildCanvas()
 {
 Холст c = новый холст();

 Линия линия = новая линия();
 линия.X1 = 1;
 линия.Y1 = 1;
 линия.X2 = 100;
 линия.Y2 = 100;
 линия.Ширина = 100;
c.Дети.Добавить(строка);

 c.Мера(новый размер(300, 300));
 c.Организуйте(новый Прямой(0, 0, 300, 300));
 }
 }
}

ПРИМЕЧАНИЕ: первый ответ ниже немного не соответствует действительности, так как я уже явно заявил, что такое же поведение происходит во время события нажатия кнопки приложения WPF. Однако я явно не указывал, что в этом приложении я выполняю только ограниченное количество итераций (скажем, 1000). Выполнение этого таким образом позволит GC запускаться при щелчке по приложению. Также обратите внимание, что я явно сказал, что взял дамп памяти и обнаружил, что мои объекты были укоренены через !gcroot. Я также не согласен с тем, что GC не сможет работать. GC не запускается в основном потоке моего консольного приложения, тем более что я нахожусь на двухъядерной машине, что означает, что параллельная рабочая станция GC активна. Насос сообщений, однако, да.

Чтобы доказать это, вот версия приложения WPF, которая запускает тест на DispatcherTimer. Он выполняет 1000 итераций в течение интервала таймера 100 мс. Более чем достаточно времени, чтобы обработать любые сообщения из насоса и снизить загрузку процессора.

использование Системы;
использование Системы.Коллекции.Общий;
использование System.Linq;
использование System.Text;
использование System.Windows;
используя Систему.Windows.Управления;
используя Систему.Windows.Формы;

пространство имен SimpleReproSampleWpfApp
{
 открытый частичный класс Window1 : Окно
 {
 частная Система.Windows.Нарезание резьбы.Время отправки _ время;

 открытое окно1()
 {
 Инициализировать компонент();

 _timer = новая система.Windows.Нарезание резьбы.Диспетчер времени();
 _тимер.Интервал = промежуток времени.от миллисекунд(100);
 _тимер.Галочка  = новый обработчик событий(_timer_Tick);
 _тимер.Начало();
 }

 [Система.Время выполнения.Услуги компиляторов.Методимпл(System.Runtime.Услуги компиляторов.Методимпопции.не наклоняясь)]
 пустое рантестирование()
 {
для (int i = 0; i
 {
 BuildCanvas();
 }
 }

 [Система.Время выполнения.Услуги компиляторов.Методимпл(System.Runtime.Услуги компиляторов.Методимпопции.не наклоняясь)]
 частная статическая пустота BuildCanvas()
 {
 Холст c = новый холст();

 Линия линия = новая линия();
 линия.X1 = 1;
 линия.Y1 = 1;
 линия.X2 = 100;
 линия.Y2 = 100;
 линия.Ширина = 100;
c.Дети.Добавить(строка);

 c.Мера(новый размер(300, 300));
 c.Организуйте(новый Прямой(0, 0, 300, 300));
 }

 void _timer_Tick(отправитель объекта, параметры события e)
 {
 _timer.Стоп();

 РанТест();

 _тимер.Начало();
 }
 }
}

ПРИМЕЧАНИЕ 2: Я использовал код из первого ответа, и моя память росла очень медленно. Обратите внимание, что 1 мс намного медленнее и меньше итераций, чем в моем примере. Вы должны дать ему поработать пару минут, прежде чем начнете замечать рост. Через 5 минут он составляет 46 МБ с начальной точки в 30 МБ.

ПРИМЕЧАНИЕ 3: Удаление вызова на .Аранжировка полностью исключает рост. К сожалению, этот вызов довольно важен для моего использования, так как во многих случаях я создаю файлы PNG с холста (с помощью класса RenderTargetBitmap). Без призыва к этому .Расположите его так, чтобы он вообще не размещал холст.

Ответ №1:

Я смог воспроизвести вашу проблему, используя предоставленный вами код. Память продолжает расти, потому что объекты Canvas никогда не освобождаются; профилировщик памяти указывает, что диспетчер ContextLayoutManager удерживает их все (чтобы при необходимости он мог вызывать OnRenderSizeChanged).

Кажется, что простой обходной путь состоит в том, чтобы добавить

 c.UpdateLayout()
 

до самого конца BuildCanvas .

Тем не менее, обратите внимание, что Canvas это a UIElement ; он должен использоваться в пользовательском интерфейсе. Он не предназначен для использования в качестве произвольной поверхности для рисования. Как уже отмечали другие комментаторы, создание тысяч объектов Canvas может указывать на недостаток дизайна. Я понимаю, что ваш производственный код может быть более сложным, но если это просто рисование простых фигур на холсте, код на основе GDI (т. Е. Система.Классы рисования) могут быть более подходящими.

Комментарии:

1. и на этом заканчивается урок о том, почему сборка мусора является плохой системой управления памятью, особенно в языках, которые претендуют на простоту в использовании.

2. Я на 100% полностью согласен с вами, и ваш ответ, похоже, решает проблему. Я вхожу в этот код 2 года назад, и, к сожалению, на данный момент нет простого способа изменить его.

3. Я видел, как мой холст был подключен к ContextLayoutManager через некоторые объекты SizeChangeInfo, но я слишком новичок в классах WPF, чтобы понять это (по-видимому). Спасибо.

4. Невероятно! Но это помогло мне избежать утечек памяти в элементе мультимедиа!

Ответ №2:

WPF в .NET 3 и 3.5 имеет утечку внутренней памяти. Это срабатывает только в определенных ситуациях. Мы никогда не могли точно понять, что вызывает это, но у нас это было в нашем приложении. Очевидно, это исправлено в .NET 4.

Я думаю, что это то же самое, что упомянуто в этом посте в блоге

Во всяком случае, помещение следующего кода в App.xaml.cs конструктор решило эту проблему за нас

 public partial class App : Application
{
   public App() 
   { 
       new HwndSource(new HwndSourceParameters()); 
   } 
}
 

Если ничто другое не решит эту проблему, попробуйте это и посмотрите

Комментарии:

1. Это, безусловно, интересно, но это было в Vista, и в этом исходном сообщении в блоге указано, что это только XP. blogs.msdn.com/b/jgoldb/archive/2008/02/04/…

Ответ №3:

Обычно в .NET GC запускается при выделении объектов при пересечении определенного порога, это не зависит от перекачки сообщений (я не могу представить, что с WPF все по-другому).

Я подозреваю, что объекты холста каким-то образом укоренены глубоко внутри или что-то в этом роде. Если вы это сделаете, с. Дети.Clear() прямо перед завершением метода BuildCanvas рост памяти резко замедляется.

В любом случае, как отметил здесь комментатор, такое использование элементов фреймворка довольно необычно. Зачем вам нужно так много Холстов?

Комментарии:

1. Это большое приложение WPF, которое со временем испытывает очень медленный рост памяти. После нескольких дней использования использование памяти невероятно велико, и после изучения дампов памяти я смог воссоздать сценарий, который вызывает рост.

2. Это приложение для рисования; файл-новый и все такое в течение длительного времени увеличивает объем памяти и продолжает расти. Акт создания новых нарисованных элементов, создания нового документа, новых нарисованных элементов и т. Д. Вызывает утечку. Этот пример кода делает то, что пользователи видят намного быстрее.

3. Занимаюсь . Дети. Ясность не имеет никакого эффекта. Вы действительно можете увидеть поведение, не добавляя вообще никаких детей на холст.

4. У нас есть приложение WPF с более чем 10 тысячами привязок к ~3000 элементам фреймворка в сцене, без какого-либо чрезмерного роста с течением времени и т. Д. Однако мы повторно используем элементы фреймворка, а не удаляем/добавляем новые элементы.

5. Есть шанс, что ты позвонишь . Устраивать метод когда-либо?

Ответ №4:

Правка 2: Очевидно, что это не ответ, но он был частью обмена ответами и комментариями здесь, поэтому я не удаляю его.

У GC никогда не будет возможности собрать эти объекты, потому что ваш цикл и его блокирующие вызовы никогда не заканчиваются, и, следовательно, перекачка сообщений и события никогда не получат свою очередь. Если бы вы использовали Timer какое-то средство, чтобы сообщения и события действительно могли обрабатываться, вы, вероятно, не смогли бы съесть всю свою память.

Правка: Следующее не съедает мою память до тех пор, пока интервал больше нуля. Даже если интервал составляет всего 1 Тик, если он не равен 0. Если это 0, мы возвращаемся к бесконечному циклу.

 public partial class Window1 : Window {
    Class1 c;
    DispatcherTimer t;
    int count = 0;
    public Window1() {
        InitializeComponent();

        t = new DispatcherTimer();
        t.Interval = TimeSpan.FromMilliseconds( 1 );
        t.Tick  = new EventHandler( t_Tick );
        t.Start();
    }

    void t_Tick( object sender, EventArgs e ) {
        count  ;
        BuildCanvas();
    }

    private static void BuildCanvas() {
        Canvas c = new Canvas();

        Line line = new Line();
        line.X1 = 1;
        line.Y1 = 1;
        line.X2 = 100;
        line.Y2 = 100;
        line.Width = 100;
        c.Children.Add( line );

        c.Measure( new Size( 300, 300 ) );
        c.Arrange( new Rect( 0, 0, 300, 300 ) );
    }
}
 

Комментарии:

1. Не говоря уже о том, что постоянное воссоздание элементов каркаса обходится ЧРЕЗВЫЧАЙНО дорого.

2. Смотрите комментарий к следующему ответу.

3. 1 Может быть, это и не ответ, но похоже на правильную попытку. Мне больно видеть это при -1.