Автор: Jossef Goldberg
Название оригинала: Finding Memory Leaks in WPF-based applications
Дата публикации: 5-2-2008 5:23 AM
Адрес источника: http://blogs.msdn.com/b/jgoldb/archive/2008/02/04/finding-memory-leaks-in-wpf-based-applications.aspx
+ [Дополнения] от переводчика (niknolaty.blogspot.com)
Существует множество блогов, которые объясняют утечки памяти в программах Microsoft .NET Framework с управляемым и неуправляемым кодом.
В этом посте я хочу:
1) Показать примеры кода, которые могут вызвать утечки памяти, возникающие в
WPF приложениях;
2) Разместить информацию об утечках памяти в .NET Framework;
3) Показать, как можно предотвратить эти утечки;
4) Обсудить инструменты и техники, позваляющие обнаружить утечки.
Я планирую обновлять данный пост большими примерами кода, поскольку мы продолжаем поиск дополнительных утечек платформы и примеров кодирования, которые вызывают утечки памяти в WPF-приложений.
Пример
Для иллюстрации я прикрепил пример приложения. Приложение содержит дочерние окна, каждое из которых разделено на определенный тип утечки памяти. В каждом из случаев закрытия этого окна, не происходит ожидаемого освобождения памяти, занимаемым объектом Window.
Для наглядности, я включил таблицу с типами утечек:
Утечка памяти
Чтобы посмотреть утечку памяти:
1) Запустите Process Explorer. Откройте диалог "Свойства процесса" (правый щелчок мыши/Свойства).
2) Откройте несколько дочерних окон.
3) Обратите внимание, память возрастает на 50 Мб при каждом запуске.
4) Закройте диалог без флажка не чекбоксе ("Очистка событий для избежания утечки памяти").
5) Щелкните на "Force GC" чтобы показать список мусора.
6) Обратите внимание, память не освобождена.
7) Повторите шаги (4) и (5), но теперь с флажком не чекбоксе. Это позволит освободить объекты, когда закроется окно. На данный момент память освобождена.
У каждого из дочерних окон получается утечка памяти по причинам, описанных ниже.
1. Использование обработчика событий
Причина
Утечка срабатывает, потому что дочернее окно Window2 имеет ссылку (зарегестрированую как событие) на окно Window1.TextBox1, которая остается висеть в памяти, вызывая объект Window2 и элемент в дереве остается живым.
В общем, если Вы напишете так:
Код:
Исправление/Решение
Есть несколько подходов, самый простой - это отменить регистрацию объекта Window2 из различных источников событий, когда окно готовится к закрытию.
Пример:
[Дополнение: в WPF есть два события закрытия. Событие Closing() происходит непосредственно после нажатия на кнопку Close и может отменить закрытие окна. Событие Closed() происходит, когда окно закрывают.]
2. Использование Data Binding
Причина
Данная утечка описана в статье KB. Она срабатывает потому что:
TextBlock имеет Binding к объекту MyGrid, который ссылается обратно на TextBlock (это один из потомков MyGrid). Обратите внимание, что этот тип утечки DataBinding является уникальным для конкретного сценария и для сценария привязки в частности, как описано в статье KB. Свойство в Path не является свойством зависимости и не является классом, который реализует INotifyPropertyChanged и, кроме того, должна существовать цепь прямых ссылок.
Код:
Есть несколько подходов, самый простой - это очистить привязку (binding), когда окно закроется.
Пример
[Дополнение: Режим привязки OneTime сообщает о том, что привязка будет осуществляться один раз при запуске программы, либо при смене контекста данных (DataContext). Это более простая форма привязки типа OneWay, она обеспечивает более высокую производительность в тех случаях, когда значение источника не меняется.]
3. Использование привязки команды
Данная утечка срабатывает, потому что объект Window2 добавляет привязку команды к окну Window1. В WPF привязка команды использует прямую ссылку, которая вызывает окно Window2, а дочерней объект еще не реализован, пока Window2 остается в памяти.
Код:
Исправление/Решение
Самым простым решением будет вызов очистки CommandBinding, когда окно будет закрыто.
Пример
Причина
Данная утечка срабатывает, потому что дочернее окно (Window2) имеет ссылку (зарегестрированую как событие) на статическое событие. Поскольку объект является статическим, объект Window2 никогда не будет освобожден.
Код:
Просто отменяем регистрацию события с объекта Window2, когда окно будет закрыто.
Пример
5. Использование BitmapImage в Image Source
Причина
Утечка срабатывает потому, что WPF сохраняет прямые ссылки между статическим BitmapImage (bi1) и Image (m_Image1). BitmapImage (bi1) определен как статический, поэтому он не очищается сборщиком мусора, когда окно Window2 будет закрыто, так как под капотом WPF хуки событий привязаны к BitmapImage (например событие DownloadFailed), а он вызывает изображение m_Image1, чтобы остаться в живых. Это, в свою очередь, заставляет все дерево Window2 висеть в памяти даже после того, когды Вы закрыли окно. Утечка может произойти только тогда, когда вы используете BitmapImage, она не появляется, когда используется DrawingImage.
Данная проблема исправлена в следующей версии .NET сервис пака (.NET 3.5 SP1).
Код:
Решение может зависит от вашего сценария. Один из способов - это использовать функцию Freeze() для BitmapImage.
WPF не перехватывает события для объектов, которые заморожены. Данное решение может использоваться, если Вы нажмете на второй чекбокс, расположенный выше. Другое решение - это клонировать (Clone()) BitmapImage или сделать его статическим. В общем, Вы должны морозить объекты по возможности, чтобы улучшить производительность приложения. Подробности смотрите здесь.
Пример
Причина
Данная утечка была упомянута выше. Она возникает, потому что WPF сохраняет прямые ссылки между статическим BitmapImage bi1 и изображением m_Image1.
Когда изображению Image присваивается новый источник (например: m_Image1.Source = bi2;), WPF "забывает" удалить предыдущее "старое" событие, которое подключали к bi1. Опять же, поскольку bi1 является статическим и не удаляется сборщиком мусора, он заставляет изображение оставаться висеть в памяти, который и вызывает утечку.
Данный вопрос был поднят в .NET версии 3.5. Он не актуален в .NET 3.0.
Эта проблема будет исправлена в следующей версии .NET сервис пака (.NET 3.5 SP1).
Код:
Чтобы решить данную проблему, можно не использовать код в вышеупомянотом виде, либо заморозить все остальные BitmapImage:
Данная утечка срабатывает, потому что WPF не удаляет внутренние ссылки на некоторые объекты (например, LateBoundBitmapDecoder, BitmapFrameDecode, и т.д.), которые используются во время загрузки с интернета. Утечка срабатывает только тогда, когда Вы скачиваете изображение с интернета (это не актуально для изображений, находящихся на локальной машине). Эта проблема будет исправлена в следующей версии .NET сервис пака (.NET 3.5 SP1).
Чтобы отследить утечку, Вы можете открыть вышеупомянутое окно, закрыть его, и щелкнуть на кнопке "Force GC", чтобы увидеть список мусора.
При запуске команды в WinDbg, расположенной ниже, Вы заметите объекты, которые остались в куче (Heap). Это объекты, которые вызывают утечку и закрепляются за объектом Image и всего дерева после закрытия окна Window2.
Обойти ее можно, осуществляя загрузку BitmapImage в начале во временную папку или в память, а затем использовать локальное изображение BitmapImage. Смотрите (WebClient.DownloadFile & WebClient.DownloadData).
8. Утечки в CMilChannel при инициализации HWND, разрушенного под XP
Причина
Это утечка в WPF присутствует во всех версиях фреймворка вплоть до .NET 3.5 SP1. Это происходит из-за того, как WPF осуществляет выбор, а именно: какой HWND использовать для отправки сообщений от визуализации потока в поток пользовательского интерфейса. Этот пример разрушит первый созданный HWND и запустит анимацию в новом окне. Он вызывает сообщения, отправленные на поток визуализации без обработки, что создает утечку памяти.
Исправление/Решение
Выходом может быть создание нового HwndSource первым в конструкторе класса App. Он должен быть создан до того, как будет создаваться любой другой HWND. Просто создавая этот HwndSource, WPF будет использовать его для передачи сообщений от визуализации потока в поток пользовательского интерфейса. Это гарантирует, что все сообщения будут обработаны.
Примечание: Эта проблема довольно таки редка, поэтому реализовывайте данное решение, если Вы на самом деле испытываете из-за этого проблемы.
9. ShutDownListener делает утечку для каждого потока, созданного через привязку
Причина
Это утечка в WPF присутствует во всех версиях фреймворка вплоть до .NET 3.5 SP1. Она происходит из-за того, что обработчик события в движке Data Binding'a подключен, но никогда не снимается, когда привязка будет использоваться в новом потоке. В этом примере создается некоторое число новых потоков, и для каждого нового окна используется привязка данных.
Исправление/Решение:
Пока недоступно
10. Создание и уничтожение WriteableBitmap на XP через аппартный рендеринг
Причина
Эта утечка присутствует только в WPF версии 3.5 SP1. Она происходит всякий раз, когда создается WriteableBitmap и разрушается на Windows XP с использованием аппаратного рендеринга. Данный пример неоднократно создает и разрушает, обновляет и показывает новые WriteableBitmap'ы, что приводит к утечке памяти.
Исправление/Решение
Для окна, содержаший WriteableBitmap, установить HwndTarget.RenderMode в RenderMode.SoftwareOnly.
11. Viewport3D, VisualBrush, WriteableBitmap, и др., утечки в Windows XP
Причина
Эта утечка присутствует в WPF версии 3.5 SP1. Она происходит, когда VisualBrush, WriteableBitmap, или любые другие объекты используются в Viewport3D в программном режиме рендеринга.
Исправление/Решение
По возможности, используйте аппартный рендеринг. Если же он не доступен, и Вы подозреваете, что данная утечка есть, попробуйте заменить Вашу кисть на SolidColorBrush, чтобы посмотреть, пропадет ли утечка. Если она не устранена, то значит есть другая. А если же она исчезла, то просто используйте другую кисть, которая не будет содержать утечки памяти. Другой способ отсутсвует.
[Дополнение: Код статьи - KB967634. Исправлено в WPF 4.0 (ссылка)]
[Дополнение] 12. Использование словарей ресурсов
Причина
Если в приложении используется словарь ресурсов (ResourceDictionary) либо словарь тем, то, так как они являются глобальными, после их использования они могут остаться висеть в памяти, когда приложение закроется.
Исправление/Решение
Дискуссия по этому поводу была здесь. Данный баг был исправлен в WPF 4.0. Код статьи - KB967328 (ссылка).
[Дополнение] 13. Ресурсы, не использующие фактических значений
Причина
Данная утечка относится к приложению, которое содержит стили/шаблоны, определенные в ресурсах (StaticResource или DynamicResource) и при этом, создается большое количество элементов, не использующие фактические значения данного ресурса. В данной ситуации, у приложения может срабатывать утечка объектов DeferredAppResourceReference, даже когда они были очищены. Кроме того, в приложении может срабатывать утечка типа WeakReferences.
Например, в ресурсах App.xaml определяют:
Название оригинала: Finding Memory Leaks in WPF-based applications
Дата публикации: 5-2-2008 5:23 AM
Адрес источника: http://blogs.msdn.com/b/jgoldb/archive/2008/02/04/finding-memory-leaks-in-wpf-based-applications.aspx
+ [Дополнения] от переводчика (niknolaty.blogspot.com)
Существует множество блогов, которые объясняют утечки памяти в программах Microsoft .NET Framework с управляемым и неуправляемым кодом.
В этом посте я хочу:
1) Показать примеры кода, которые могут вызвать утечки памяти, возникающие в
WPF приложениях;
2) Разместить информацию об утечках памяти в .NET Framework;
3) Показать, как можно предотвратить эти утечки;
4) Обсудить инструменты и техники, позваляющие обнаружить утечки.
Я планирую обновлять данный пост большими примерами кода, поскольку мы продолжаем поиск дополнительных утечек платформы и примеров кодирования, которые вызывают утечки памяти в WPF-приложений.
Пример
Для иллюстрации я прикрепил пример приложения. Приложение содержит дочерние окна, каждое из которых разделено на определенный тип утечки памяти. В каждом из случаев закрытия этого окна, не происходит ожидаемого освобождения памяти, занимаемым объектом Window.
Для наглядности, я включил таблицу с типами утечек:
Описание утечки
|
Ошибка разработчика
|
NET 3.0
|
NET 3.5
|
NET 3/5 sp1
|
Неправильное использование обработчиков событий
|
Да
|
|||
Неправильное использование привязки данных
|
Да
|
|||
Неправильное использование привязки команды
|
Да
|
Да
|
Да
|
Да
|
Неправильное использование статических обработчиков событий
|
Да
|
|||
Использование BitmapImage в источнике Image
|
Да
|
Да
|
||
Множественное использование BitmapImage
|
Да
|
|||
Использование скачанного BitmapImage
|
Да
|
|||
Утечки в CMilChannel при инициализации HWND, разрушенного под Windows XP
|
Да (XP)
|
Да (XP)
|
Да (XP)
| |
ShutDownListener делает утечку для каждого потока, созданного через привязку
|
Да
|
Да
|
Да
| |
Создание и уничтожение WriteableBitmap на Windows XP через аппартный рендеринг
|
Да (XP с HW)
| |||
Viewport3D, VisualBrush, WriteableBitmap, и др., утечки в Windows XP
|
Да (XP с HW)
|
Утечка памяти
Чтобы посмотреть утечку памяти:
1) Запустите Process Explorer. Откройте диалог "Свойства процесса" (правый щелчок мыши/Свойства).
2) Откройте несколько дочерних окон.
3) Обратите внимание, память возрастает на 50 Мб при каждом запуске.
4) Закройте диалог без флажка не чекбоксе ("Очистка событий для избежания утечки памяти").
5) Щелкните на "Force GC" чтобы показать список мусора.
6) Обратите внимание, память не освобождена.
7) Повторите шаги (4) и (5), но теперь с флажком не чекбоксе. Это позволит освободить объекты, когда закроется окно. На данный момент память освобождена.
У каждого из дочерних окон получается утечка памяти по причинам, описанных ниже.
1. Использование обработчика событий
Причина
Утечка срабатывает, потому что дочернее окно Window2 имеет ссылку (зарегестрированую как событие) на окно Window1.TextBox1, которая остается висеть в памяти, вызывая объект Window2 и элемент в дереве остается живым.
В общем, если Вы напишете так:
Foo.SomeEvent += new EventHandler(Bar.SomeMethod)
То, когда Вы закончите использовать Bar, но будете использовать Foo, Bar также останется жив. Наверное это не то, что Вы могли ожидать. Код:
Window1.w1.TextBox1.TextChanged += new TextChangedEventHandler(this.TextBox1_TextChanged);
Объект Window2 будет оставаться в памяти, пока TextBox1 в Window1 будет жить.Исправление/Решение
Есть несколько подходов, самый простой - это отменить регистрацию объекта Window2 из различных источников событий, когда окно готовится к закрытию.
Пример:
Второй подход заключается в создании своего рода трюка (например, "Слабые ссылки"). См. блог Грега Шехтера (Greg Schechter's) для примера.Window1.w1.TextBox1.TextChanged
-=
new TextChangedEventHandler(TextBox1_TextChanged);
[Дополнение: в WPF есть два события закрытия. Событие Closing() происходит непосредственно после нажатия на кнопку Close и может отменить закрытие окна. Событие Closed() происходит, когда окно закрывают.]
2. Использование Data Binding
Причина
Данная утечка описана в статье KB. Она срабатывает потому что:
TextBlock имеет Binding к объекту MyGrid, который ссылается обратно на TextBlock (это один из потомков MyGrid). Обратите внимание, что этот тип утечки DataBinding является уникальным для конкретного сценария и для сценария привязки в частности, как описано в статье KB. Свойство в Path не является свойством зависимости и не является классом, который реализует INotifyPropertyChanged и, кроме того, должна существовать цепь прямых ссылок.
Код:
myDataBinding = new Binding("Children.Count");
myDataBinding.Source = myGrid;
myDataBinding.Mode = BindingMode.OneWay;
MyTextBlock.SetBinding(TextBlock.TextProperty, myDataBinding);
Похожий код можно записать на XAML:<TextBlock Name="MyTextBlock" Text="{Binding ElementName=myGrid, Path=Children.Count}" />
Исправление/РешениеЕсть несколько подходов, самый простой - это очистить привязку (binding), когда окно закроется.
Пример
BindingOperations.ClearBinding(MyTextBlock, TextBlock.TextProperty);
Другой подход - это установить режим привязки в OneTime. Подробней можно посмотреть в статье KB.[Дополнение: Режим привязки OneTime сообщает о том, что привязка будет осуществляться один раз при запуске программы, либо при смене контекста данных (DataContext). Это более простая форма привязки типа OneWay, она обеспечивает более высокую производительность в тех случаях, когда значение источника не меняется.]
3. Использование привязки команды
Данная утечка срабатывает, потому что объект Window2 добавляет привязку команды к окну Window1. В WPF привязка команды использует прямую ссылку, которая вызывает окно Window2, а дочерней объект еще не реализован, пока Window2 остается в памяти.
Код:
command = new RoutedCommand("ClearBox", this.GetType());
command.InputGestures.Add(new KeyGesture(Key.F5));
myCmdBinding = new CommandBinding(command, F5CommandExecute);
Window1.w1.CommandBindings.Add(myCmdBinding); //добавление привязки к Window1
Заметка: Вероятно, это не является распространенной практикой, но данный код написан при условии, чтобы продемонстрировать идею использования привязки команды, которая может привести к утечкам памяти.Исправление/Решение
Самым простым решением будет вызов очистки CommandBinding, когда окно будет закрыто.
Пример
Window1.w1.CommandBindings.Remove(myCmdBinding);
4. Использование статического обработчика событийПричина
Данная утечка срабатывает, потому что дочернее окно (Window2) имеет ссылку (зарегестрированую как событие) на статическое событие. Поскольку объект является статическим, объект Window2 никогда не будет освобожден.
Код:
Application.Current.Activated += new EventHandler(App_Activated);
Исправление/РешениеПросто отменяем регистрацию события с объекта Window2, когда окно будет закрыто.
Пример
Application.Current.Activated -= new EventHandler(App_Activated);
Второй подход был указан ранее в пункте [1].5. Использование BitmapImage в Image Source
Причина
Утечка срабатывает потому, что WPF сохраняет прямые ссылки между статическим BitmapImage (bi1) и Image (m_Image1). BitmapImage (bi1) определен как статический, поэтому он не очищается сборщиком мусора, когда окно Window2 будет закрыто, так как под капотом WPF хуки событий привязаны к BitmapImage (например событие DownloadFailed), а он вызывает изображение m_Image1, чтобы остаться в живых. Это, в свою очередь, заставляет все дерево Window2 висеть в памяти даже после того, когды Вы закрыли окно. Утечка может произойти только тогда, когда вы используете BitmapImage, она не появляется, когда используется DrawingImage.
Данная проблема исправлена в следующей версии .NET сервис пака (.NET 3.5 SP1).
Код:
// bi1 статический
bi1 = new BitmapImage(new Uri("Bitmap1.bmp",UriKind.RelativeOrAbsolute));
// bi1.Freeze() // если не использовать Freeze, в Вашем приложение будет утечка памяти
m_Image1 = new Image();
m_Image1.Source = bi1;
MyStackPanel.Children.Add(m_Image1);
Исправление/РешениеРешение может зависит от вашего сценария. Один из способов - это использовать функцию Freeze() для BitmapImage.
WPF не перехватывает события для объектов, которые заморожены. Данное решение может использоваться, если Вы нажмете на второй чекбокс, расположенный выше. Другое решение - это клонировать (Clone()) BitmapImage или сделать его статическим. В общем, Вы должны морозить объекты по возможности, чтобы улучшить производительность приложения. Подробности смотрите здесь.
Пример
bi1.Freeze();
6.
Множественное использование BitmapImage в Image Source
Причина
Данная утечка была упомянута выше. Она возникает, потому что WPF сохраняет прямые ссылки между статическим BitmapImage bi1 и изображением m_Image1.
Когда изображению Image присваивается новый источник (например: m_Image1.Source = bi2;), WPF "забывает" удалить предыдущее "старое" событие, которое подключали к bi1. Опять же, поскольку bi1 является статическим и не удаляется сборщиком мусора, он заставляет изображение оставаться висеть в памяти, который и вызывает утечку.
Данный вопрос был поднят в .NET версии 3.5. Он не актуален в .NET 3.0.
Эта проблема будет исправлена в следующей версии .NET сервис пака (.NET 3.5 SP1).
Код:
static BitmapImage bi1 = new BitmapImage(new Uri("Bitmap1.bmp", UriKind.RelativeOrAbsolute));
static BitmapImage bi2 = new BitmapImage(new Uri("Bitmap2.bmp", UriKind.RelativeOrAbsolute));
…
if (bi2.CanFreeze)
bi2.Freeze();
// bi1.Freeze() // даже если используете для bi2 источник изображения,
// Вы также должны заморозить bi1
m_Image1 = new Image();
m_Image1.Source = bi1; // использование не замороженного bitmap - вызовет утечку
m_Image1.Source = bi2; // используйте замороженный bitmap
MyStackPanel.Children.Add(m_Image1);
Исправление/РешениеЧтобы решить данную проблему, можно не использовать код в вышеупомянотом виде, либо заморозить все остальные BitmapImage:
bi1.Freeze();
7. Использование скачанного BitmapImage в Image SourceДанная утечка срабатывает, потому что WPF не удаляет внутренние ссылки на некоторые объекты (например, LateBoundBitmapDecoder, BitmapFrameDecode, и т.д.), которые используются во время загрузки с интернета. Утечка срабатывает только тогда, когда Вы скачиваете изображение с интернета (это не актуально для изображений, находящихся на локальной машине). Эта проблема будет исправлена в следующей версии .NET сервис пака (.NET 3.5 SP1).
Чтобы отследить утечку, Вы можете открыть вышеупомянутое окно, закрыть его, и щелкнуть на кнопке "Force GC", чтобы увидеть список мусора.
При запуске команды в WinDbg, расположенной ниже, Вы заметите объекты, которые остались в куче (Heap). Это объекты, которые вызывают утечку и закрепляются за объектом Image и всего дерева после закрытия окна Window2.
.loadby sos mscorwks
!DumpHeap -type System.Windows.Media.Imaging
53dadf18 6 72 System.Windows.Media.UniqueEventHelper`1 [[System.Windows.Media.Imaging.DownloadProgressEventArgs, PresentationCore]]
53da4374 1 108 System.Windows.Media.Imaging.PngBitmapDecoder
53da09e0 4 112 System.Windows.Media.Imaging.BitmapSourceSafeMILHandle
53d8d2f0 1 120 System.Windows.Media.Imaging.LateBoundBitmapDecoder
53da0524 1 172 System.Windows.Media.Imaging.BitmapFrameDecode
53da89c8 3 648 System.Windows.Media.Imaging.BitmapImage
Код:// Утечка сработает, когда будете использовать BitmapImage, загруженного с интернета
BitmapImage image = new BitmapImage();
image.BeginInit();
image.UriSource = new Uri(@http://www.somesite.com/some_image.png, UriKind.RelativeOrAbsolute);
image.CacheOption = BitmapCacheOption.OnLoad;
image.CreateOptions = BitmapCreateOptions.None;
image.EndInit();
m_Image1 = new Image();
m_Image1.Source = image;
MyStackPanel.Children.Add(m_Image1);
Исправление/РешениеОбойти ее можно, осуществляя загрузку BitmapImage в начале во временную папку или в память, а затем использовать локальное изображение BitmapImage. Смотрите (WebClient.DownloadFile & WebClient.DownloadData).
8. Утечки в CMilChannel при инициализации HWND, разрушенного под XP
Причина
Это утечка в WPF присутствует во всех версиях фреймворка вплоть до .NET 3.5 SP1. Это происходит из-за того, как WPF осуществляет выбор, а именно: какой HWND использовать для отправки сообщений от визуализации потока в поток пользовательского интерфейса. Этот пример разрушит первый созданный HWND и запустит анимацию в новом окне. Он вызывает сообщения, отправленные на поток визуализации без обработки, что создает утечку памяти.
Исправление/Решение
Выходом может быть создание нового HwndSource первым в конструкторе класса App. Он должен быть создан до того, как будет создаваться любой другой HWND. Просто создавая этот HwndSource, WPF будет использовать его для передачи сообщений от визуализации потока в поток пользовательского интерфейса. Это гарантирует, что все сообщения будут обработаны.
Примечание: Эта проблема довольно таки редка, поэтому реализовывайте данное решение, если Вы на самом деле испытываете из-за этого проблемы.
9. ShutDownListener делает утечку для каждого потока, созданного через привязку
Причина
Это утечка в WPF присутствует во всех версиях фреймворка вплоть до .NET 3.5 SP1. Она происходит из-за того, что обработчик события в движке Data Binding'a подключен, но никогда не снимается, когда привязка будет использоваться в новом потоке. В этом примере создается некоторое число новых потоков, и для каждого нового окна используется привязка данных.
Исправление/Решение:
Пока недоступно
10. Создание и уничтожение WriteableBitmap на XP через аппартный рендеринг
Причина
Эта утечка присутствует только в WPF версии 3.5 SP1. Она происходит всякий раз, когда создается WriteableBitmap и разрушается на Windows XP с использованием аппаратного рендеринга. Данный пример неоднократно создает и разрушает, обновляет и показывает новые WriteableBitmap'ы, что приводит к утечке памяти.
Исправление/Решение
Для окна, содержаший WriteableBitmap, установить HwndTarget.RenderMode в RenderMode.SoftwareOnly.
11. Viewport3D, VisualBrush, WriteableBitmap, и др., утечки в Windows XP
Причина
Эта утечка присутствует в WPF версии 3.5 SP1. Она происходит, когда VisualBrush, WriteableBitmap, или любые другие объекты используются в Viewport3D в программном режиме рендеринга.
Исправление/Решение
По возможности, используйте аппартный рендеринг. Если же он не доступен, и Вы подозреваете, что данная утечка есть, попробуйте заменить Вашу кисть на SolidColorBrush, чтобы посмотреть, пропадет ли утечка. Если она не устранена, то значит есть другая. А если же она исчезла, то просто используйте другую кисть, которая не будет содержать утечки памяти. Другой способ отсутсвует.
[Дополнение: Код статьи - KB967634. Исправлено в WPF 4.0 (ссылка)]
[Дополнение] 12. Использование словарей ресурсов
Причина
Если в приложении используется словарь ресурсов (ResourceDictionary) либо словарь тем, то, так как они являются глобальными, после их использования они могут остаться висеть в памяти, когда приложение закроется.
Исправление/Решение
Дискуссия по этому поводу была здесь. Данный баг был исправлен в WPF 4.0. Код статьи - KB967328 (ссылка).
[Дополнение] 13. Ресурсы, не использующие фактических значений
Причина
Данная утечка относится к приложению, которое содержит стили/шаблоны, определенные в ресурсах (StaticResource или DynamicResource) и при этом, создается большое количество элементов, не использующие фактические значения данного ресурса. В данной ситуации, у приложения может срабатывать утечка объектов DeferredAppResourceReference, даже когда они были очищены. Кроме того, в приложении может срабатывать утечка типа WeakReferences.
Например, в ресурсах App.xaml определяют:
<sys:Double x:Key="{x:Static SystemParameters.HorizontalScrollBarHeightKey}">20</sys:Double>
Этот ресурс используют любые элементы управления, которые могут отображать горизонтальную полосу прокрутки. Создание большого количества таких элементов управления, которые на самом деле никогда не будут показывать эту прокрутки и вызовет утечку.Данный баг был исправлен в WPF 4.0. Код статьи - KB981107 (ссылка).
[Дополнение] 14. Котороткоживущие элементы, связанные с DependencyProperty
Причина
Это относится к приложению, которое создает большое количество короткоживущих элементов со свойствами, которые связаны с DependencyProperty из долгоживущих элементов. В данной ситуации, более долгоживущие элементы могут вести списки WeakReference объектов, используемые для уведомления короткоживущих элементов об изменении свойств.
Например: ListBoxItem связывает свое свойство HorizontalContentAlignment с аналогичным свойством в своем ListBox. Приложение, которое создает большое количество ListBoxItem (скажем, делая большое количество добавлений и удалений в ItemsSource), будут сталкиваться с утечкой памяти.
Исправление/Решение
Данный баг был исправлен в WPF 4.0. Код статьи - KB981107 (ссылка).
[Дополнение] 15. Классы с элементами, использующие KeyboardNavigation
Причина
Эта проблема касается классов, производных от TreeView или Selector (в том числе ListBox, ListView, Menu, DataGrid, и многих других). Эти классы зарегестрированы на внутренние уведомления от класса KeyboardNavigation, который используется для управления фокусом клавиатуры. Приложение, которое создает большое количество таких элементов, является уязвимым к утечке памяти объектов типа WeakReference и массива пространства, для их хранения. Например: если приложение создает большое количество короткоживущих ListBox'ов, то Вы непременно столкнетесь с утечкой. Это может произойти во вложенном сценарии, где в ListBox отображается коллекция, в которой происходит большое количество операций удаления и добавления - и все это отображается внутри ListBox.
Исправление/Решение
Данный баг был исправлен в WPF 4.0. Код статьи - KB981107 (ссылка).
Отладка утечек памяти
В поисках утечек памяти, я в основном использовал CLR Profiler для .NET Framework 2.0 и WinDbg. Преимущество еще и в том, что они оба являются бесплатными.
Полезные советы:
Гораздо легче обнаружить утечку, если специально сделать объект очень большим. Например: добавьте 50 Мб к его размеру, и Вы сможете наблюдать признаки утечки памяти. В моем примере я выделил примерно 50 Мб памяти в каждом дочернем окне (Byte[]).
Если у вас есть небольшая утечка, то может потребоваться много итераций, прежде чем сделать вывод, действительно ли она существует, используя Process Explorer или диспетчер задач.
Заставляя сборщик мусора освободить память, Вы получаете разницу между объектами и можете определить, у которых есть утечка, а у которых нет. Пример кода:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Использование сборщика также полезно, когда вы осмотриваете память, например с помощью Process Explorer, и одновременно используете CLR Profiler - это позволит использовать GC между каждым снимком кучи (Heap).Использование CLR Profiler
1. Запустите CLR Memory Profiler под админом на Висте
2. Уберите флажки с чекбоксов "Allocations", "Calls" и "Profiling Active"
3. Нажмите "Start Application" и перейдите к точке, где Вы готовы быть до снимка кучи (Heap)
4. Щелкните "Show Heap Now"
5. Теперь установите флаги "Profiling Active" и "Allocations" для включения профилирования
6. Запустите и закройте окно с утечкой (например: Event "Handler test")
7. Выберите кнопку "Show Heap Now"
8. Щелкните правой кнопкой мыши на последнем графике и щелкните "Show New Objects"
Теперь Вы видите, что обработчик TextChangedEventHandler остановился с размером 50 Мб буфера Byte[]:
Повторите данный процесс для окна "Command Binding test", и он покажет 50 Мб для объекта CommandBinding. Смотрите на скрин:
Использование WinDBG
В большей степени, я следовал инструкциям, опубликованных здесь.
windbg -p <your process id>
0:004> .loadby sos mscorwks
Я выполнил:0:005> !DumpHeap –stat
Дважды (перед и после утечки)."!DumpHeap –stat" покажет состояние кучи (Heap) перед срабатыванием утечки:
…
5695e56c 460 18400 System.Windows.DependencyProperty
5696975c 188 20280 System.Windows.EffectiveValueEntry[]
79135df4 99 34440 System.Reflection.CustomAttributeNamedParameter[]
0056ed60 297 37656 Free
7913b600 177 65376 System.Collections.Hashtable+bucket[]
7912b884 3307 152020 System.Object[]
790fc6cc 8516 455296 System.String
Total 32362 objects
После утечки команда "!DumpHeap –stat" покажет следующее:5543b1e8 189 11340 System.Windows.Markup.BamlAttributeInfoRecord
53d0d3ac 40 11424 System.Windows.ClassHandlers[]
569698f4 384 11888 MS.Utility.FrugalMapBase+Entry[]
790febbc 627 12540 System.RuntimeType
5695e7c0 628 12560 System.Windows.DependencyProperty+FromNameKey
5696975c 244 15928 System.Windows.EffectiveValueEntry[]
5542d18c 676 16224 System.Windows.FrameworkPropertyMetadata
5695e56c 484 19360 System.Windows.DependencyProperty
7913b600 80 38952 System.Collections.Hashtable+bucket[]
7912b884 785 73608 System.Object[]
0056ed60 288 103380 Free
790fc6cc 7218 373856 System.String
7913b858 57 52433700 System.Byte[]
Подозрения на размещение буфера Byte[] размером 50 Мб на лицо. Позже я выполнил:Потом я выполнил команду gcroot с адресом на наш буфер и WinDbg отрапортавал:0:005> !dumpheap -type System.Byte[] … 013894d4 7913b858 60 0138965c 7913b858 228 013897c0 7913b858 60 01389a70 7913b858 60 0138f6d4 7913b858 500 06dc1000 7913b858 52428816 total 57 objects Statistics: MT Count TotalSize Class Name
7913b858 57 52433700 System.Byte[]
Total 57 objects
0:005> !gcroot 06dc1000
Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info.
Scan Thread 0 OSTHread 1280
ESP:37f2d8:Root:012f6d68(System.Windows.Threading.Dispatcher)->
0130c6b0(System.Windows.Input.InputManager)->
0130cd58(System.Windows.Input.StylusLogic)->
0130ce8c(System.Collections.Generic.Dictionary`2[[System.Object, mscorlib],[System.Windows.Input.PenContexts, PresentationCore]])->
0130ced8(System.Collections.Generic.Dictionary`2+Entry[[System.Object, mscorlib],[System.Windows.Input.PenContexts, PresentationCore]][])->
0135e1e8(System.Windows.Interop.HwndSource)->
012fab4c(TestWpfApp.Window1)->
01334b90(System.Windows.Controls.TextBox)->
0136f664(System.Windows.EffectiveValueEntry[])->
0134deb0(System.Windows.EventHandlersStore)->
01383340(MS.Utility.ThreeObjectMap)->
01383320(MS.Utility.FrugalObjectList`1[[System.Windows.RoutedEventHandlerInfo, PresentationCore]])->
0138332c(MS.Utility.SingleItemList`1[[System.Windows.RoutedEventHandlerInfo, PresentationCore]])->
01383300(System.Windows.Controls.TextChangedEventHandler)->
0137e2d8(TestWpfApp.Window2)->
06dc1000(System.Byte[])
Scan Thread 2 OSTHread 1500
DOMAIN(005656C8):HANDLE(WeakSh):c1794:Root:01384aec(System.EventHandler)->
01384828(System.Windows.Documents.AdornerLayer)->
0137e2d8(TestWpfApp.Window2)
Ту же самую информацию показал мне и CLR memory Profiler.Другие инструменты
Существует и другие программы для работы с памятью: SciTech’s Memory Profiler, Red-Gate’s ANTS Profiler, YourKit Profiler, JetBrains dotTrace 3.0 и другие. У всех имеется удобный и богатый на функционал пользовательский интерфейс, а также есть улучшенная поддержка, чем у инструментов, приведенных выше.
Другие типы утечек
Существуют и другие типы утечек в управляемом коде, но они выходят за рамки данного поста. Один из типичных случаев утечки, это когда объект находится в неуправляемых ресурсах.
Это может произойти в следующих случаях:
1) Управляемые объекты храняться в неуправляемых ресурсах и не удаляются из памяти, как обычно это предполагается (как правило, в IDisposable реализации).
2) Очень маленький объект, который хранится в большом количестве неуправляемой памяти. Сборщик мусора видит только малое количество управляемой памяти и не понимает, какая конкретно требуется коллекция. Как правило, этот сценарий подходит к точечным (Bitmap) изображениям, так как растровые изображения имеют небольшой управляемый компонент, который отвечает за большой размер неуправляемой памяти. Данное положение улучшилось в .NET 2.0 с введением AddMemoryPressure API, который позволяет объектам сообщить сборщику мусора размер неуправляемого кластера.
В WPF уже по умолчанию используется сборщик мусора, вследствие чего, рассматривать вопрос об использовании API AddMemoryPressure нужно, только если у вас используется вышеупомянутый сценарий.
Выводы
Во всех выше приведенных моделей, основной проблемой является использование прямых ссылок на объекты, которые остаются в памяти. Некоторые из прямых ссылок, реализованны в WPF Framework'е; однако концепция прямых ссылок, используемых обработчиком события не является новым в WPF и существует с первой версии .NET Framework и WinForms.
Некоторые утечки памяти в WPF, существуют, как правило из-за новой концепции программирования и внутренней реализации WPF. Кроме того, обычное приложение WPF гораздо богаче в графике и медиа, поэтому, если в приложении есть утечка памяти, она обычно имеет значительные размеры и ее легко заметить. Это делает утечки более важными для разработчиков приложений, чтобы быть осторожным и избегать их в своих приложениях.
Ссылки
Пост в блоге Яна Гриффитса: "Simulating Weak Delegates for EventHandler-like Delegates"
Грег Шехтер: Simulating “Weak Delegates” in the CLR
Блог Rico Mariani: http://blogs.msdn.com/b/ricom/
Инструмент CLR Profiler for the .NET Framework 2.0
Инструмент WinDbg
Благодарю Адама Смита (Adam Smith), Эрика Хардинга (Eric Harding) и Момина Аль-Хусейна (Momin Al-Ghosien) - они помогали мне в данном обзоре, а так же Майку Куку (Mike Cook), который участвовал в создании данного поста.
Комментариев нет:
Отправить комментарий