Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
CheckBootSpeed - это диагностический пакет на основе скриптов PowerShell, создающий отчет о скорости загрузки Windows 7 ...
Вы когда-нибудь хотели создать установочный диск Windows, который бы автоматически установил систему, не задавая вопросо...
Если после установки Windows XP у вас перестала загружаться Windows Vista или Windows 7, вам необходимо восстановить заг...
Программа подготовки документов и ведения учетных и отчетных данных по командировкам. Используются формы, утвержденные п...
Red Button – это мощная утилита для оптимизации и очистки всех актуальных клиентских версий операционной системы Windows...
OSzone.net Microsoft Разработка приложений Windows (до Windows 10) Управление памятью в приложениях Windows Store, часть 2 RSS

Управление памятью в приложениях Windows Store, часть 2

Текущий рейтинг: 0 (проголосовало 0)
 Посетителей: 748 | Просмотров: 1326 (сегодня 0)  Шрифт: - +
PerfView.

В первой статье из этой серии обсуждалось, как происходят утечки памяти, почему они замедляют работу вашего приложения и ухудшают общую производительность системы. Также были рассмотрены универсальные способы предотвращения утечек и специфических проблем в приложениях на JavaScript. Теперь мы изучим утечки памяти в контексте приложений на C#, Visual Basic и C++. Мы проанализируем некоторые базовые ситуации утечек, которые наблюдались в приложениях прошлых поколений, и посмотрим, как технологии Windows 8 помогают избегать таких ситуаций. Опираясь на этот фундамент, мы перейдем к более сложным сценариям, которые могут вызывать утечку памяти в приложении. Итак, приступим!

Простые циклы

В прошлом многие утечки вызывались циклическими ссылками (reference cycles). Объекты, участвующие в цикле, всегда имеют активную ссылку, даже если объекты в цикле могли быть недостижимыми. Активная ссылка навечно удерживала эти объекты в памяти, и, если программа часто создавала такие циклы, со временем утечка памяти только нарастала.

Циклическая ссылка может возникать по нескольким причинам. Самая очевидная из них — когда объекты явно ссылаются друг на друга. Например, следующий код дает результаты, показанные на рис. 1:

Foo a = new Foo();
Bar b = new Bar();
a.barVar = b;
b.fooVar = a;

*
Рис. 1. Круговая ссылка

К счастью, в языках с поддержкой сбора мусора, таких как C#, JavaScript и Visual Basic, этот вид круговых ссылок будет автоматически очищаться, как только необходимость в переменных отпадет.

C++/CX, напротив, не использует сбор мусора. Вместо этого он полагается при управлении памятью на учет ссылок. Это означает, что объекты будут удаляться системой, только когда у них будут нулевые счетчики активных ссылок. В этих языках циклы между объектами заставили бы A и B существовать вечно, потому что их счетчики ссылок не обнулились бы никогда. Хуже того, все, на что ссылаются A и B, тоже будет существовать вечно. Это упрощенный пример того, чего можно легко избежать при написании базовых программ; однако сложные программы могут создавать циклы, включающие множество объектов, связанных друг с другом далеко не очевидными путями. Давайте рассмотрим некоторые примеры.

Циклы с обработчиками событий

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

Рис. 2. Создание круговой ссылки с помощью обработчика событий

<MainPage x:Class="App.MainPage" ...>
  ...
  <TextBlock x:Name="displayTextBlock" ... />
  <Button x:Name="myButton" Click="ButtonClick" ... />
  ...
</MainPage>
public sealed partial class MainPage : Page
{
  ...
  private void ButtonClick(object sender, RoutedEventArgs e)
  {
    DateTime currentTime = DateTime.Now;
    this.displayTextBlock.Text = currentTime.ToString();
  }
  ...
}

Здесь мы просто добавили Button и TextBlock в Page. Мы также настроили обработчик, определенный в классе Page, для события Click от Button. Этот обработчик обновляет текст в TextBlock для отображения текущего времени при каждом щелчке Button. Как видите, даже в таком простом примере есть круговая ссылка.

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

*
Рис. 3. Круговая ссылка, относящаяся к обработчику события

TextBlockTextBlock
MainPageMainPage
ButtonButton
ButtonClickButtonClick
MethodМетод
DelegateДелегат
ClickClick


В нижней части той же схемы видно, что при регистрации обработчика события, определенного в классе Page, создается еще одна ссылка.

Источник события (Button) имеет жесткую ссылку (strong reference) на этот обработчик, метод делегата, чтобы источник мог вызывать обработчик при срабатывании события. Давайте назовем этот делегат жестким, так как ссылка от него является жесткой.

Создание обработчиков события — чрезвычайно распространенный сценарий, и Microsoft не хочет, чтобы это приводило к утечкам в приложении независимо от используемого языка.

Теперь мы имеем круговую ссылку. Как только пользователь уходит со страницы, сборщик мусора (GC) поступает достаточно интеллектуально и разрывает цикл между Page и Button. Эти типы круговых ссылок будут автоматически очищаться, когда необходимости в них больше нет — при условии, что вы пишете приложения на JavaScript, C# или Visual Basic. Однако, как мы уже отмечали, C++/CX — язык с учетом ссылок, а значит, объект автоматически удаляются, только когда их счетчики ссылок обнуляются. Здесь созданные вами жесткие ссылки заставили бы Page и Button существовать вечно, так как их счетчики ссылок никогда бы не обнулились. Хуже того, все элементы, содержащиеся в Page (а потенциально это очень большое дерево элементов), тоже существовали бы вечно, поскольку Page удерживал бы ссылки на эти объекты.

Конечно, создание обработчиков события — чрезвычайно распространенный сценарий, и Microsoft не хочет, чтобы это приводило к утечкам в приложении независимо от используемого языка. По этой причине компилятор XAML делает ссылку от делегата на слушатель событий слабой. Вы можете считать этот делегат слабым, поскольку его ссылка является слабой.

Слабый делегат гарантирует, что страница не останется в памяти из-за удерживания ссылки на нее таким делегатом.

Слабый делегат гарантирует, что страница не останется в памяти из-за удерживания ссылки на нее таким делегатом. Слабая ссылка не учитывается счетчиком ссылок страницы и поэтому она позволит уничтожить страницу, как только остальные счетчики ссылок обнулятся. Соответственно Button, TextBlock и любые другие объекты, на которые ссылается страница, тоже будут уничтожены.

Долгоживущие источники событий

Иногда события определяются в объекте с длительным сроком жизни. Мы называем такие события долгоживущими, так как события существуют столько же, сколько и объект, в котором они определены. Эти долгоживущие события хранят ссылки на все зарегистрированные обработчики. Это заставляет обработчики и объекты, являющиеся целевыми для этих обработчиков, существовать столько, сколько существует долгоживущий источник событий.

В подразделе «Обработчики событий» предыдущей статьи по утечкам памяти мы проанализировали один такой пример. Каждая страница в приложении регистрируется на получение события SizeChangedEvent окна приложения. Ссылка от SizeChangedEvent окна на обработчик события в странице будет удерживать каждый экземпляр Page, пока существует окно приложения. Все страницы, на которые были переходы, остаются в памяти, даже если видима лишь одна из них. Эта утечка легко исправляется отменой регистрации обработчика события SizeChangedEvent каждой страницы, когда пользователь покидает ее.

В этом примере очевидно, когда страница больше не нужна и разработчик может отменить регистрацию обработчика событий в странице. К сожалению, определить срок жизни объекта не всегда так просто. Подумайте об имитации «слабого делегата» в C# или Visual Basic, если вы обнаружите утечки, вызванные долгоживущими событиями, которые удерживают объекты через обработчики этих событий. (См. статью «Simulating ‘Weak Delegates’ in the CLR» по ссылке bit.ly/SUqw72.) Шаблон слабого делегата размещает промежуточный объект между источником событий и обработчиком. Используйте жесткую ссылку от источника событий к промежуточному объекту и слабую ссылку от промежуточного объекта к обработчику, как показано на рис. 4.

*
Рис. 4. Использование промежуточного объекта между источником событий и слушателем событий

Diagram 1Схема 1
EventAEventA
LongLived ObjectДолгоживущий объект
MethodМетод
DelegateДелегат
EventA HandlerОбработчик EventA
ShortLived ObjectКороткоживущий объект
Diagram 2Схема 2
Intermediate ObjectПромежуточный объект
This object is leaked due to the strong reference held by the delegateЭтот объект дает утечку из-за ссылки, удерживаемой делегатом
This object is garbage collected because the intermediate object only holds a weak reference to itЭтот объект подвергается сбору мусора, так как промежуточный объект хранит лишь слабую ссылку на него


На первой схеме на рис. 4 LongLivedObject предоставляет EventA, и ShortLivedObject регистрирует EventAHandler для обработки этого события. LongLivedObject имеет гораздо больший срок жизни, чем ShortLivedObject, и строгая ссылка между EventA и EventAHandler будет удерживать ShortLivedObject до тех пор, пока существует LongLivedObject. Размещение IntermediateObject между LongLivedObject и ShortLivedObject (как показано на схеме 2) позволяет создать утечку в IntermediateObject вместо ShortLivedObject. Это куда менее значимая утечка, поскольку IntermediateObject должен предоставлять только одну функцию, тогда как ShortLivedObject может содержать большие структуры данных или сложное визуальное дерево.

Рассмотрим, как можно было бы реализовать в коде слабый делегат. Событие, регистрация на которое может понадобиться во многих класса, — DisplayProperties.OrientationChanged. DisplayProperties на самом деле является статическим классом, поэтому событие OrientationChanged будет существовать всегда. Это событие будет удерживать ссылку на каждый объект, который его прослушивает. В примере на рис. 5 и 6 класс LargeClass использует шаблон слабого делегата, чтобы гарантировать, что событие OrientationChanged сохраняет строгую ссылку только на промежуточный класс, когда происходит регистрация обработчика события. Затем промежуточный класс вызывает метод, определенный в LargeClass и реально выполняющий всю необходимую работу при инициации события OrientationChanged.

*
Рис. 5. Шаблон слабого делегата

OrientationChangedOrientationChanged
DisplayPropertiesDisplayProperties
MethodМетод
DelegateДелегат
WeakOrientation ChangedHandlerWeakOrientation ChangedHandler
WeakDelegate WrapperОболочка WeakDelegate
LargeClassLargeClass


Рис. 6. Реализация слабого делегата

public class LargeClass
{
  public LargeClass()
  {
    // Создаем промежуточный объект
    WeakDelegateWrapper wrapper = new WeakDelegateWrapper(this);
    // Регистрируем обработчик в промежуточном объекте
    // с DisplayProperties.OrientationChanged вместо
    // обработчика в LargeClass
    Windows.Graphics.Display.DisplayProperties.OrientationChanged +=
      wrapper.WeakOrientationChangedHandler;
  }
  void OrientationChangedHandler(object sender)
  {
    // Здесь что-то делаем
  }
  class WeakDelegateWrapper : WeakReference<LargeClass>
  {
    DisplayPropertiesEventHandler wrappedHandler;
    public WeakDelegateWrapper(LargeClass wrappedObject,
      DisplayPropertiesEventHandler handler) :base(wrappedObject)
    {
      wrappedHandler = handler;
      wrappedHandler += WeakOrientationChangedHandler;
    }
    public void WeakOrientationChangedHandler(object sender)
    {
      LargeClass wrappedObject = Target;
      // Вызываем реальный обработчик события в LargeClass,
      // если он еще существует и не был удален при сборе
      // мусора. Удаляем обработчик, если LargeClass удален
      // при сборе мусора, чтобы слабый делегат
      // больше не давал утечки.
      if(wrappedObject != null)
        wrappedObject.OrientationChangedHandler(sender);
      else
        wrappedHandler -= WeakOrientationChangedHandler;
    }
  }
}

Лямбды

Многие считают, что обработчики событий легче реализовать с помощью лямбды (или подставляемой функции) вместо какого-либо метода. Давайте преобразуем пример с рис. 2, чтобы сделать именно так (рис. 7).

Рис. 7. Реализация обработчика события с помощью лямбды

MainPage x:Class="App.MainPage" ...>
  ...
  <TextBlock x:Name="displayTextBlock" ... />
  <Button x:Name="myButton" ... />
  ...
</MainPage>
public sealed partial class MainPage : Page
{
  ...
  protected override void OnNavigatedTo
  {
    myButton.Click += => (source, e)
    {
      DateTime currentTime = DateTime.Now;
      this.displayTextBlock.Text = currentTime.ToString();
    }
  ...
}

Использование лямбды тоже создает цикл. Первые ссылки по-прежнему создаются от Page на Button и TextBlock (по аналогии с верхней схемой на рис. 3).

Следующий набор ссылок, показанных на рис. 8, неявно создается лямбдой. Событие Click объекта Button подключается к объекту RoutedEventHandler, чей метод Invoke реализуется замыканием во внутреннем объекте, создаваемым компилятором. Замыкание (closure) должно содержать ссылки на все переменные, на которые ссылается лямбда. Одна из этих переменных — this, которая в контексте лямбды ссылается на Page, тем самым создавая цикл (круговую ссылку).

*
Рис. 8. Ссылки, создаваемые лямбдой

TextBlockTextBlock
MainPageMainPage
ClickClick
ButtonButton
InvokeInvoke
RoutedEventHandlerRoutedEventHandler
_foo$a22_foo$a22
Closure currentTime, thisЗамыкание currentTime, this
currentTime is only around during the execution of the event handlercurrentTime существует только на время выполнения обработчика событий


Если лямбда написана на C# или Visual Basic, то CLR GC будет отзывать ресурсы, участвующие в цикле. Однако в C++/CX этот вид ссылки является жестким и вызовет утечку. Это не означает, что все лямбды на C++/CX дают утечки. Круговая ссылка не создавалась бы, если бы мы не ссылались на this и использовали бы в замыкании только локальные переменные при определении лямбды. Как одно из решений этой проблемы, если вам нужно обращаться к внешней для замыкания переменной в подставляемом обработчике событий, реализуйте этот обработчик в форме метода. Это позволяет компилятору XAML создать слабую ссылку от события к его обработчику, и память будет корректно отзываться. Другой вариант — использование синтаксиса указателя на член (pointer-to-member syntax), который позволяет указывать, жесткая или слабая ссылка создается на класс, содержащий метод с синтаксисом указателя на член (в данном случае — на класс Page).

Использование параметра события sender

Как обсуждалось в предыдущей статье, каждый обработчик события принимает параметр, который, как правило, называется sender; он представляет источник события. Параметр источника события в лямбде помогает избегать круговых ссылок. Давайте модифицируем наш пример (используя C++/CX ) так, чтобы кнопка показывала текущее время при ее щелчке (рис. 9).

Рис. 9. Кнопка, показывающее текущее время

<MainPage x:Class="App.MainPage" ...>
  ...
  <Button x:Name="myButton" ... />
  ...
</MainPage>
MainPage::MainPage()
{
   ...
   myButton->Click += ref new RoutedEventHandler(
     [this](Platform::Object^ sender,
     Windows::UI::Xaml::RoutedEventArgs^ e)
   {
     Calendar^ cal = ref new Calendar();
     cal->SetToNow() ;
     this->myButton->Content = cal->SecondAsString();
   });
   ...
}

Обновленная лямбда создает те же круговые ссылки, что и на рис. 8. Они будут приводить к утечке в коде на C++/CX, но этого можно избежать, используя параметр источника вместо ссылки на myButton через переменную this. При выполнении метод замыкания создает в стеке параметры «source» и «e». Эти переменные существуют только во время вызова метода, а не столько времени, сколько лямбда подключена к обработчику событий в Button (currentTime имеет тот же срок жизни). Вот код, использующий параметр источника:

MainPage::MainPage()
{
  ...
  myButton->Click += ref new RoutedEventHandler([](Platform::Object^ sender,
   Windows::UI::Xaml::RoutedEventArgs^ e)
  {
    DateTime currentTime ;
    Calendar^ cal = ref new Calendar();
    cal->SetToNow() ;
    Button ^btn = (Button^)sender ;
    btn->Content = cal->SecondAsString();  });
  ...
}

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

*
Рис. 10. Использование параметра источника

TextBlockTextBlock
MainPageMainPage
ClickClick
ButtonButton
InvokeInvoke
RoutedEvent HandlerRoutedEventHandler
_foo$a22_foo$a22
Closure source, e, cal, btnЗамыкание source, e, cal, btn
source, e, cal and btn are only around during the execution of the event handlersource, e, cal и btn существуют только на время выполнения обработчика событий

Применение WRL для предотвращения утечек в коде на стандартном C++

Создавать приложения Windows Store можно не только на JavaScript, C#, C++/CX и Visual Basic, но и на стандартном C++. В последнем случае применяются привычные методики COM, такие как учет ссылок, для управления сроками жизни объектов и проверки HRESULT-значений, чтобы определить, была ли операция успешной. Windows Runtime C++ Template Library (WRL) упрощает написание этого кода (bit.ly/P1rZrd). Мы рекомендуем использовать эту библиотеку при создании приложений Windows Store на стандартном C++, чтобы сократить количество возможных ошибок и уменьшить риск утечек памяти, которые крайне трудно локализовать и устранить.

Многие считают, что обработчики событий легче реализовать с помощью лямбды (или подставляемой функции) вместо какого-либо метода.

Будьте осторожны при использовании обработчиков событий, пересекающих границы языков

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

Рассмотрим, как это может случиться (рис. 11).

Рис. 11. Отображение местоположения пользователя

<Page x:Class="App.MainPage" ...>
  ...
  <TextBlock x:Name="displayTextBlock" ... />
  ...
</Page>
public sealed partial class MyPage : Page
{
  ...
  Geolocator gl;
  protected override void OnNavigatedTo{} ()
  {
    Geolocator gl = new Geolocator();
    gl.PositionChanged += UpdatePosition;
  }
  private void UpdatePosition(object sender, RoutedEventArgs e)
  {
    // Изменяем текст в TextBlock
    // для отображения текущей позиции
  }
  ...
}

Этот пример очень похож на предыдущие. Page содержит TextBlock, который отображает минимальный объем информации. Однако в этом примере TextBlock показывает местоположение пользователя, как представлено на рис. 12.

*
Рис. 12. Круговые ссылки пересекают границу сборщика мусора

WinRT (CLR)WinRT (CLR)
WinRT (Native)WinRT (неуправляемый код)
TextBlockTextBlock
Update PositionUpdatePosition
MyPageMyPage
Position ChangedPositionChanged
GeolocatorGeolocator


К этому моменту, наверное, вы и сами могли бы найти круговые ссылки. Однако неочевидная часть заключается в том, что круговые ссылки распространяются за GC-границу. Поскольку ссылки простираются за пределы CLR, ее GC не может обнаружить наличие цикла, и это приведет к утечке. Предотвращать утечки таких типов трудно, так как не всегда можно сказать, на каком языке реализованы объект и его события. Если Geolocator написан на C# или Visual Basic, круговые ссылки останутся в рамках CLR, и цикл попадет под сбор мусора. Если же этот класс написан на C++ (как в данном случае) или JavaScript, цикл вызовет утечку.

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

Есть несколько способов обезопасить свое приложение от таких утечек. Начнем с того, что вам не надо беспокоиться об этих утечках, если вы пишете приложение на чистом JavaScript. JavaScript GC зачастую достаточно «интеллектуален», чтобы отслеживать круговые ссылки между всеми WinRT-объектами. (Подробнее об управлении памятью в JavaScript см. предыдущую статью.)

Кроме того, вам нет нужды волноваться, если вы регистрируетесь на события в объектах, которые находятся в инфраструктуре XAML. Это подразумевает все, что находится в пространстве имен Windows.UI.Xaml, и включает все привычные вам классы FrameworkElement, UIElement и Control. CLR GC достаточно «интеллектуален» для отслеживания круговых ссылок через XAML-объекты.

Другой способ предотвращения этого типа утечки — отмена регистрации обработчика событий, когда необходимость в нем исчезает. В этом примере вы могли бы отменить регистрацию обработчика в событии OnNavigatedFrom. Тогда ссылка, созданная этим обработчиком, была бы удалена, и все объекты были бы уничтожены. Заметьте, что отменять регистрацию лямбд нельзя, поэтому обработка события с помощью лямбды может вызывать утечки.

Анализ утечек памяти в приложениях Windows Store на C# и Visual Basic

Если вы пишете приложение Window Store на C# или Visual Basic, полезно иметь в виду, что многие методологии для JavaScript, описанные в предыдущей статье, равно применимы для C# и Visual Basic. В частности, использование слабых ссылок — распространенный и эффективный способ уменьшения роста занимаемой памяти (подробнее см. по ссылке bit.ly/S9gVZW); кроме того, вы можете применять те же архитектурные шаблоны Dispose и Bloat.

Теперь рассмотрим, как можно находить и устранять распространенные утечки с помощью доступных на сегодняшний день инструментов: Windows Task Manager и средства профилирования управляемого кода, PerfView, доступного для скачивания по ссылке bit.ly/UTdb4M.

В подразделе «Обработчики событий» предыдущей статьи по утечкам мы изучили пример под названием LeakyApp (для удобства он повторяется на рис. 13), которые вызывает утечку памяти в обработчике события SizeChanged окна.

Рис. 13. LeakyApp

public sealed partial class ItemDetailPage : LeakyApp.Common.LayoutAwarePage
  {
    public ItemDetailPage()
    {
      this.InitializeComponent();
    }
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
      base.OnNavigatedTo(e);
      Window.Current.SizeChanged += WindowSizeChanged;
    }
    private void WindowSizeChanged(object sender,
      Windows.UI.Core.WindowSizeChangedEventArgs e)
    {
      // Реагируем на изменение размера
    }
// Прочий код
  }

По нашему опыту, это наиболее распространенный тип утечки в коде на C# и Visual Basic, но методики, которые мы будем описывать, также применимы к утечкам, связанным с круговыми ссылками и ростом неограниченных структур данных. Итак, обсудим, как находить и устранять утечки в приложениях с помощью доступных сегодня инструментов.

Обнаружение роста занимаемой памяти

Первый шаг в устранении утечки памяти — идентификация четкого роста объема занимаемой памяти при операциях, которые должны быть нейтральны к памяти. В разделе «Обнаружение утечек памяти» предыдущей статьи мы обсудили очень простой способ: использование встроенного Windows Task Manager для наблюдения за ростом общего рабочего набора (Total Working Set, TWS) приложения, многократно выполняемого по определенному сценарию. В приложении-примере операции, которые приводили к утечке памяти, были щелчок по плитке (тайлу) и последующий переход обратно на главную страницу.

На рис. 14 верхний экранный снимок показывает рабочий набор в Task Manager до 10 итераций этих действий, а нижний — после 10 итераций.

*
*
Рис. 14. Наблюдение за ростом занимаемой памяти

После 10 итераций видно, что объем занятой памяти вырос с 44 404 Кб до 108 644 Кб. Это определенно утечка памяти, и мы должны копать глубже.

Добавление детерминированности сборщику мусора

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

Самый простой способ — использовать кнопку Force GC в PerfView (см. следующий раздел). Другой вариант — добавить кнопку в приложение, которая программным способом инициирует сбор мусора:

private void GCButton_Click(object sender, RoutedEventArgs e)
{
  GC.Collect();
  GC.WaitForPendingFinalizers();
  GC.Collect();
}

Вызов WaitForPendingFinalizers и последующий вызов Collect гарантируют, что любые объекты, освобожденные в результате срабатывания методов подготовки к уничтожению (finalizers), будут собраны GC.

Однако в приложении-примере щелчок этой кнопки после 10 итераций освободил всего 7 Мб из 108 Мб рабочего набора. И теперь мы совершенно уверены, что в LeakyApp есть утечка памяти. Далее нужно найти причину утечки памяти в нашем управляемом коде.

Анализ роста занимаемой памяти

Теперь мы воспользуемся PerfView для дифференциации GC-кучи CLR и проведем анализ для поиска объектов, дающих утечки.

Чтобы найти, где происходит утечка памяти, нужно сделать моментальные снимки кучи до и после выполнения операции, вызывающей утечку в приложении. С помощью PerfView вы можете определить разницу между двумя снимками и найти, где происходит рост занимаемой памяти.

Чтобы сделать снимок кучи с помощью PerfView:

  • откройте PerfView;
  • щелкните Memory в строке меню;
  • щелкните Take Heap Snapshot (рис. 15);
  • выберите из списка свое приложение Windows Store;
  • щелкните кнопку Force GC для запуска GC в рамках вашего приложения;
  • присвойте имя файлу дампа, сохраните его и щелкните Dump GC Heap (рис. 16).

*
Рис. 15. Снятие снимка кучи

*
Рис. 16. Создание дампа GC-кучи

Дамп управляемой кучи будет сохранен в указанный файл, и PerfView откроет его. Вы увидите список всех типов в управляемой куче. Для исследования утечки памяти вы должны удалить содержимое текстовых полей Fold% и FoldPats, а затем щелкнуть кнопку Update. В результате в столбце Exc будет показан общий размер памяти (в байтах), которую использует данный тип в GC-куче, а в столбце Exc Ct будет отображено количество экземпляров этого типа в GC-куче.

На рис. 17 дано представление GC-дампа для LeakyApp.

*
Рис. 17. Снимок кучи в PerfView

Чтобы увидеть разницу между двумя снимках кучи, показывающую утечку памяти:

  • выполните несколько итераций действия, которое вызывает утечку памяти в вашем приложении. Это приведет к включению в основные данные любых объектов с отложенной загрузкой или разовой инициализацией;
  • сделайте снимок кучи, а затем принудительно запустите GC для удаления любых «мертвых» объектов. Мы будем называть этот снимок «до выполнения итераций»;
  • выполните еще несколько итераций действия, вызывающего утечку;
  • сделайте еще один снимок кучи, а затем принудительно запустите GC для удаления любых «мертвых» объектов. Мы будем называть этот снимок «после выполнения итераций»;
  • в представлении снимка после выполнения итераций выберите команду меню Diff и укажите в качестве основных данных снимок до выполнения итераций. Убедитесь, что у вас открыто представление снимка до выполнения итераций, а иначе он не появится в меню;
  • разница между снимками будет показана в новом окне. Удалите содержимое текстовых полей Fold% и FoldPats, а затем обновите представление.

Теперь у вас есть представление, показывающее рост памяти, занимаемой управляемыми объектами, между двумя снимками управляемой кучи. В случае LeakyApp мы сделали первый снимок после трех итераций, а второй — через 13, что дает разницу в GC-куче после 10 итераций. Разностный снимок в PerfView приведен на рис. 18.

*
Рис. 18. Разностный снимок в PerfView

В столбце Exc видно увеличение общего размера памяти, выделенной под каждый тип в управляемой куче. Однако столбец Exc Ct покажет сумму экземпляров в двух снимках кучи, а не разницу между ними. Это не то, что вы ожидали бы при таком анализе, и будущие версии PerfView позволят изучать разностные значения в этом столбце; пока что просто игнорируйте столбец Exc Ct при использовании разностного представления.

Любые типы, которые текут между двумя снимками, покажут положительные значения в столбце Exc, но определение того, какой именно объект мешает сбору этих объектов, потребует дополнительного анализа.

Анализ разницы

Исходя из знания этого приложения, вы должны изучить список объектов в разностном снимке и найти типы, которые не должны были бы давать роста занимаемой памяти со временем. Ищите сначала типы, определенные в вашем приложении, так как утечка чаще всего является результатом какой-то ссылки, удерживаемой в коде. Следующее место поисков — типы в пространстве имен Windows.UI.Xaml, дающие утечку, так как они тоже скорее всего удерживаются в коде вашего приложения. Если вы сначала изучите только типы, определенные в вашем приложении, то увидите, что тип ItemDetailPage показывается почти в самом верху списка. Это самый крупный объект с утечкой, определенный в приложении-примере.

Дважды щелкнув этот тип в списке, вы увидите для него представление ReferedFrom. Это представление показывает дерево ссылок всех типов, которые хранят ссылки на данный тип. Вы можете раскрыть это дерево и пройти по всем ссылкам, удерживающим этот тип. В дереве значение [CCW (ObjectType)] означает, что данный объект удерживается в памяти ссылкой извне управляемого кода (например, из инфраструктуры XAML, кода на C++ или JavaScript). На рис. 19 показан снимок дерева ссылок для нашего подозреваемого объекта ItemDetailPage.

*
Рис. 19. Дерево ссылок для типа ItemDetailPage в PerfView

Здесь четко видно, что ItemDetailPage удерживается обработчиком для событий WindowSizeChanged и это скорее всего является причиной утечки памяти. Обработчик событий удерживается чем-то извне управляемого кода — в данном случае инфраструктурой XAML. Если взглянуть на один из XAML-объектов, станет понятно, что он тоже удерживается тем же обработчиком. В качестве примера дерево ссылок для типа Windows.UI.Xaml.Controls.Button приведено на рис. 20.

*
Рис. 20. Дерево ссылок для типа Windows.UI.Xaml.Controls.Button

Отсюда понятно, что все новые экземпляры UI.Xaml.Controls.Button удерживаются ItemDetailPage, а тот удерживается WindowSizeChangedEventHandler.

В этот момент очевидно, что для устранения утечки памяти мы должны удалить ссылку из обработчика событий SizeChanged на ItemDetailPage:

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
  Window.Current.SizeChanged -= WindowSizeChanged;
}

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

Описанные здесь методики дают вам представление о некоторых простых способах анализа утечек памяти. Не удивляйтесь, если вы сами попадете в сходные ситуации. Это очень распространенная утечка памяти в приложениях Windows Store, вызываемая подпиской на долгоживущие источники событий и отсутствие отмена подписки на них; к счастью, цепочка «протекающих» объектов четко указывает источник проблемы. Сюда также относятся случаи с круговыми ссылками в обработчиках событий между языками, а также традиционные утечки памяти в коде на C#/Visual Basic из-за применения неограниченных структур данных для кеширования.

В более сложных случаях утечки памяти могут вызываться циклами между объектами в приложениях, содержащих смесь кода на C#, Visual Basic, JavaScript и C++. Эти случаи зачастую трудно анализировать из-за обилия объектов в дереве ссылок, которые показываются как внешние для управляемого кода.

Соображения по приложениям Windows Store, написанным на JavaScript и C# или Visual Basic

В приложении, которое создано на JavaScript и использует C# или Visual Basic для реализации нижележащих компонентов, важно помнить, что в нем два разных GC управляют двумя разными кучами. Это естественным образом увеличивает объем памяти, занимаемой таким приложением. Однако самым важным фактором в потреблении памяти вашим приложением останется управление большими структурами данных и сроками их жизни. При пересечении границ между языками нужно учитывать следующее.

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

В случае приложений с двумя GC понимание этой разницы очень важно. Из-за ссылок между кучами удаление всех собираемых объектов может потребовать целой последовательности сборов мусора. Для проверки этого эффекта и такой очистки TWS, чтобы оставались только активные объекты, вы должны инициировать серию сборов мусора в тестовом коде. Запустить GC можно, например, в ответ на щелчок кнопки или с помощью инструмента анализа производительности, который поддерживает такую функцию. Для запуска GC в JavaScript используйте следующий код:

window.CollectGarbage();
а для CLR-языков:
GC.Collect(2, GCCollectionMode.Optimized);
GC.WaitForPendingFinalizers();

Вероятно, вы заметили, что для CLR мы используем только вызов GC.Collect в отличие от кода, применявшегося для запуска GC в разделе по диагностике утечек памяти. Это связано с тем, что здесь мы хотим имитировать реальные шаблоны сбора мусора в приложении, которые единовременно запускают GC только раз, тогда как ранее нам нужно было очистить как можно больше объектов. Заметьте, что функцию Force GC в PerfView не следует применять для замеров отложенной очистки, так как это может вызвать сбор мусора как в JavaScript-коде, так и в CLR-коде.

Ту же методологию следует применять для оценки занимаемой памяти при приостановке приложения. В среде только с C# или только JavaScript сборщик мусора будет автоматически запускаться при приостановке. Однако в гибридных приложениях на JavaScript и C# или Visual Basic произойдет запуск GC только в JavaScript. А значит, в CLR-куче останутся некоторые собираемые элементы, которые увеличат закрытый рабочий набор (PWS) вашего приложения в приостановленном состоянии. В зависимости от того, насколько велики эти элементы, ваше приложение может быть не приостановлено, а преждевременно завершено (детали см. в разделе «Избегайте хранения ссылок на крупные объекты при приостановке» предыдущей статьи).

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

Необходимость анализа обеих куч При исследовании потребления памяти и после исключения влияния отложенной очистки важно проанализировать как JavaScript-кучу, так и .NET-кучу. Рекомендуемый подход для .NET-кучи — использовать утилиту PerfView.

В текущей версии PerfView вы можете изучать комбинированное представление куч JavaScript и .NET, что позволяет видеть все объекты между управляемыми языками и понимать любые ссылки между ними.

Автор: Чипало Стрит, Дэн Тейлор  •  Иcточник: MSDN Magazine  •  Опубликована: 18.03.2013
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:  


Оценить статью:
Вверх
Комментарии посетителей
Комментарии отключены. С вопросами по статьям обращайтесь в форум.