Как освободить память, используемую тяжелыми элементами управления WPF детерминированным способом? - PullRequest
7 голосов
/ 24 января 2012

У меня возникали проблемы, когда элементы управления, выделяющие большие объемы памяти, разрушались и восстанавливались с использованием событий ввода из пользовательского интерфейса.

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

Я разбил его на этот пример:

XAML:

<Window x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseMove">
</Window>

C #:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private class HeavyObject : UserControl
        {
            private byte[] x = new byte[750000000];

            public HeavyObject()
            {
                for (int i = 0; i++ < 99999999; ) { } // just so we can follow visually in Process Explorer
                Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove
            }
        }

        public MainWindow()
        {
            InitializeComponent();

            //Works 1:
            //while (true)
            //{
            //    window.Content = null;
            //    window.Content = new HeavyObject();
            //}
        }

        private void window_MouseMove(object sender, MouseEventArgs e)
        {
            if (window.Content == null)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                // Works 2:
                //new HeavyObject();
                //window.Content = "Peekaboo!"; // change Content to el cheapo re-trigger MouseMove
                //return;
                window.Content = new HeavyObject();
            }
            else
            {
                window.Content = null;
            }
        }
    }
}

Это выделяет объект размером ~ 750 МБ (HeavyObject) для каждого события MouseMove и помещает его в качестве содержимого основного окна.Если курсор остается в окне, изменение содержимого будет бесконечно повторно вызывать событие.Ссылка на HeavyObject обнуляется перед его следующей конструкцией, и запускается тщетная попытка собрать его останки.

Что мы видим в ProcessExplorer / Taskman, так это то, что иногда выделяется 1,5 ГБ или более.Если нет, дико двигайте мышь, выходите и снова входите в окно.Если вы скомпилируете для x86 без LARGEADDRESSAWARE, вместо этого вы получите исключение OutOfMemoryException (ограничение ~ 1,3 ГБ).

С таким поведением вы не столкнетесь, если вы выделите HeavyObject в бесконечном цикле без использования ввода мышью (раскомментируйте раздел «Работает 1») или, если вы инициируете выделение мышью, но не помещаете объект в визуальное дерево (раскомментируйте раздел «Работает 2»).

Так что я полагаю, это как-то связано с визуальным деревом, лениво освобождающим ресурсы.Но затем возникает еще один странный эффект: если 1,5 ГБ расходуется, когда курсор находится за пределами окна, кажется, что ГХ не включится, пока не будет запущен MouseMove.По крайней мере, потребление памяти, по-видимому, стабилизируется до тех пор, пока не будут инициированы дальнейшие события.Так что либо где-то осталось долгоживущее упоминание, либо GC становится ленивым, когда нет активности.

Мне кажется достаточно странным.Можете ли вы выяснить, что происходит?

Редактировать: Как прокомментировал BalamBalam: данные за элементом управления могут быть разыменованы перед уничтожением элемента управления.Это был бы план Б. Тем не менее, возможно, есть более общее решение.

Сказать, что такие элементы управления - это плохой код, не поможет.Я хотел бы знать, почему объект, на который у меня нет ссылок, получает GCed, если после разыменования я оставляю пользовательский интерфейс в течение нескольких тиков, но остается навсегда, если другой объект (который должен был быть создан с помощью пользовательского ввода) немедленно принимает егоместо.

1 Ответ

7 голосов
/ 24 января 2012

Хорошо, я думаю, что могу знать, что здесь происходит. Возьми это с щепоткой соли, хотя ...

Я немного изменил ваш код.

Xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" x:Name="window" MouseDown="window_MouseDown">
</Window>

Код позади

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private class HeavyObject : UserControl
        {
            private byte[] x = new byte[750000000];

            public HeavyObject()
            {
                for (int i = 0; i < 750000000; i++ )
                {                    
                    unchecked
                    {
                        x[i] = (byte)i;       
                    }
                }

                // Release optimisation will not compile the array
                // unless you initialize and use it. 
                Content = "Loadss a memory!! " + x[1] + x[1000]; 
            }
        }

        const string msg = " ... nulled the HeavyObject ...";

        public MainWindow()
        {
            InitializeComponent();
            window.Content = msg;
        }

        private void window_MouseDown(object sender, MouseEventArgs e)
        {
            if (window.Content.Equals(msg))
            {
                window.Content = new HeavyObject();
            }
            else
            {
                window.Content = msg;
            }
        }
    }
}

Теперь запустите это и нажмите на окно. Я изменил код на , не используя GC.Collect () и на установил память на mousedown . Я компилирую в Режим выпуска и запускаю без отладчика .

Медленно нажмите в окне .

При первом щелчке вы увидите, что использование памяти увеличилось на 750 МБ. Последующие щелчки переключают сообщение между .. Nulled the heavy object.. и Loads a memory! , пока вы нажимаете медленно. Существует небольшая задержка, когда память выделяется и освобождается. Использование памяти не должно превышать 750 МБ, но оно также не уменьшается при обнулении тяжелого объекта. Это сделано специально, поскольку GC Large Object Heap является ленивым и собирает память больших объектов, когда требуется новая память больших объектов . Из MSDN:

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

Быстро нажмите в окне

Около 20 кликов. Что просходит? На моем ПК использование памяти не увеличивается, но главное окно больше не обновляет сообщение. Это замерзает. Я не получаю исключение нехватки памяти, и общее использование памяти моей системы остается неизменным.

Подчеркните систему, используя MouseMove в окне

Теперь замените MouseDown для события MouseMove (согласно коду вашего вопроса), изменив этот Xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" x:Name="window" MouseMove="window_MouseDown">
</Window>

Я могу навести указатель мыши на него на некоторое время, но в конечном итоге выдает исключение OutofMemoryException . Хороший способ вызвать исключение - сделать несколько ходов, а затем попытаться максимизировать окно.

Теория

Хорошо, это моя теория. Поскольку ваш поток пользовательского интерфейса работает на полную мощность, создавая и удаляя память внутри события цикла сообщений (MouseMove), сборщик мусора не может идти в ногу. Сборка мусора в куче больших объектов выполняется , когда требуется память . Это также дорогая задача, поэтому она выполняется как можно реже.

Задержка, которую мы заметили при быстром нажатии на вкл / выкл / вкл / выкл, становится гораздо более выраженной при выполнении в MouseMove. Если будет выделен другой большой объект, в соответствии с этой статьей MSDN GC либо попытается собрать (если память недоступна), либо начнет использовать диск.

Система находится в состоянии нехватки памяти: это происходит, когда я получаю уведомление о высокой памяти из ОС. Если я думаю, что создание второго поколения GC будет продуктивным, я сработаю один.

Теперь эта часть интересна, и я думаю, что здесь, но

Наконец, на данный момент, LOH не уплотняется как часть коллекции, но это деталь реализации, на которую не следует полагаться. Поэтому, чтобы убедиться, что что-то не перемещено GC, всегда прикрепляйте это. Теперь возьмите свое новое знание LOH и возьмите под контроль кучу.

Итак, если куча больших объектов не сжимается и вы запрашиваете несколько больших объектов в тесном цикле, возможно ли, что коллекции приводят к фрагментации, в конечном итоге приводящей к исключению OutOfMemoryException, даже если теоретически должно быть достаточно памяти?

Решение

Давайте немного освободим поток пользовательского интерфейса, чтобы позволить комнате GC дышать. Оберните код выделения в асинхронный вызов Dispatcher и используйте низкий приоритет, такой как SystemIdle. Таким образом, только выделяет память, когда поток пользовательского интерфейса свободен.

    private void window_MouseDown(object sender, MouseEventArgs e)
    {
        Dispatcher.BeginInvoke((ThreadStart)delegate()
            {
                if (window.Content.Equals(msg))
                {
                    window.Content = new HeavyObject();
                }
                else
                {
                    window.Content = msg;
                }
            }, DispatcherPriority.ApplicationIdle);
    }

Используя это, я могу танцевать указатель мыши по всей форме , и он никогда не вызывает исключение OutOfMemoryException .Действительно ли это работает или "исправило" проблему, я не знаю, но это стоит проверить.

В заключение

  • Обратите внимание, что объекты такого размера будут размещаться в куче больших объектов
  • Выделение LOH вызывает GCесли недостаточно места
  • LOH не сжимается во время цикла GC, поэтому может произойти фрагментация, приводящая к исключению OutOfMemoryException, несмотря на то, что теоретически достаточно памяти
  • В идеале повторно использовать большие объекты и нет перераспределить.Если это невозможно:
    • Allcate ваши крупные объекты в фоновом потоке, чтобы отделить от потока пользовательского интерфейса
    • Чтобы позволить комнате GC дышать, используйте Dispatcher, чтобы отделить распределение до приложенияпростаивает
...