Кажется, что привязки ReactiveUI препятствуют сбору мусора

В настоящее время мы используем ReactiveUI, чтобы помочь создать довольно большое приложение Windows на базе WPF. Все шло хорошо, пока мы не обнаружили, что наше приложение потребляет огромное количество памяти... в основном все наши взгляды, модели-модели и модели не собирались собирать мусор.

Основываясь на информации из профилировщиков памяти, таких как Jet Brains dotMemory, ReactiveUI является основным виновником. В частности, привязки The ReactiveUI, которые мы настраиваем в наших представлениях, хотя мы используем лучшие практики и гарантируем, что все привязки расположены, когда представление отключено.

Ниже приведен пример одного из видов, которые мы создаем. Любые мысли о том, где мы могли бы пойти не так, были бы очень благодарны.

public partial class RunbookInputsView : IViewFor<RunbookInputsViewModel>
{
    public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(
        "ViewModel", typeof(RunbookInputsViewModel), typeof(RunbookInputsView));

    public RunbookInputsView()
    {
        InitializeComponent();

        this.WhenActivated(d =>
        {
            d(this.OneWayBind(ViewModel, vm => vm.AddInput, v => v.AddInput.Command));                
            d(this.OneWayBind(ViewModel, vm => vm.Inputs, v => v.Inputs.ItemsSource));
        });
    }

    object IViewFor.ViewModel
    {
        get { return ViewModel; }
        set { ViewModel = (RunbookInputsViewModel)value; }
    }

    public RunbookInputsViewModel ViewModel
    {
        get { return (RunbookInputsViewModel) GetValue(ViewModelProperty); }
        set { SetValue(ViewModelProperty, value); }
    }
}

Ответ 1

Из-за этого трудно сказать, откуда вытекает утечка. Пусть утечка произойдёт некоторое время, затем присоедините к процессу windbg (часть Инструменты отладки для Windows) (Примечание: вы можете необходимо построить x86 или x64, чтобы это работало.)

После того, как вы подключились, настройте для .net-отладки, введя команды:

.symfix
sxe clr
sxd av
.loadby sos clr

Затем вы можете использовать !dumpheap -stat, чтобы использовать память каждого типа. Это производит вывод в следующем формате: (Я усекал имена классов и список для удобочитаемости.)

0:012> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
000007fefa55d2e8        1           24 System.[...]TransportSinkProvider
000007fefa55ce08        1           24 System.Run[...]rtSinkProvider
000007fee7c32df0        1           24 System.LocalDataStoreHolder
000007fee7c2ff78        1           24 System.Colle[...]
000007fee7c2ece0        1           24 System.Resources.FastResourceComparer
000007fee7c2ead0        1           24 System.Resources.ManifestBasedResourceGroveler
000007fee7c2ea70        1           24 System.[...]eManagerMediator
000007fee4cc1b70        4         1216 System.Xml.XmlDocument

Если у вас есть утечка памяти, здесь вы увидите просочившиеся объекты. (Их должно быть много.) Как только вы определили, что происходит, вы можете сделать !dumpheap -type, чтобы получить список реальных объектов. (В этом примере я использую System.Xml.XmlDocument. Имя типа чувствительно к регистру и должно быть полностью квалифицированным.)

0:012> !dumpheap -type System.Xml.XmlDocument
         Address               MT     Size
0000000002af9050 000007fee4cc1b70      304     
0000000002afa628 000007fee4cc1b70      304     
0000000002b0ea30 000007fee4cc1b70      304     
00000000037e2780 000007fee4cc1b70      304     

Statistics:
              MT    Count    TotalSize Class Name
000007fee4cc1b70        4         1216 System.Xml.XmlDocument

Ваш список, вероятно, будет намного больше, но вероятность говорит о том, что любой случайный экземпляр просочившегося типа будет чем-то интересным. Если мы сделаем !do на одном из этих адресов, мы получим вывод это выглядит так:

0:012> !do 2af9050
Name:        System.Xml.XmlDocument
MethodTable: 000007fee4cc1b70
EEClass:     000007fee4ae7f00
Size:        304(0x130) bytes
File:        C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Xml\v4.0_4.0.0.0__b77a5c561934e089\System.Xml.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
000007fee4cc2b40  40004fc        8   System.Xml.XmlNode  0 instance 0000000000000000 parentNode
000007fee4cc2258  400050a       10 ...XmlImplementation  0 instance 0000000002af9180 implementation
000007fee4cc22f0  400050b       18 ....Xml.DomNameTable  0 instance 0000000002af92e0 domNameTable
[Entries removed for clarity]
000007fee4cc26f0  400052f      108 ...m.Xml.XmlResolver  0 instance 0000000000000000 resolver
000007fee7c18c48  4000530      126       System.Boolean  1 instance                0 bSetResolver
000007fee7c113e8  4000531      110        System.Object  0 instance 0000000002af9788 objLock
000007fee4cc11b0  4000532      118 ....Xml.XmlAttribute  0 instance 0000000000000000 namespaceXml

Вы можете использовать !do для любого из объектов, перечисленных в таблице, для получения дополнительной информации. Такие типы, как System.String и System.Boolean, выплюнут их фактические значения. Если не ясно, из объекта, где он был создан, следующим шагом, вероятно, будет использование !gcroot -nostacks для поиска ссылок на наши объекты.

0:012> !gcroot -nostacks 2af9050
HandleTable:
    00000000006117d8 (pinned handle)
    -> 0000000012a55748 System.Object[]
    -> 0000000002af9050 System.Xml.XmlDocument

Found 1 unique roots (run '!GCRoot -all' to see all roots).

Там довольно много команд, и это уже слишком долго. Команда !help предоставляет хороший список. (Чтобы использовать любой из них, вам нужно будет префикс команды !. !help [command] содержит подробную информацию о конкретной команде. Например !help dumpobj:

0:012> !help dumpobj
-------------------------------------------------------------------------------
!DumpObj [-nofields] <object address>

This command allows you to examine the fields of an object, as well as learn 
important properties of the object such as the EEClass, the MethodTable, and 
the size.

You might find an object pointer by running !DumpStackObjects and choosing
from the resultant list. Here is a simple object:

    0:000> !DumpObj a79d40
    Name: Customer
    MethodTable: 009038ec
    EEClass: 03ee1b84
    Size: 20(0x14) bytes
     (C:\pub\unittest.exe)
    Fields:
          MT    Field   Offset                 Type  VT     Attr    Value Name
    009038ec  4000008        4             Customer   0 instance 00a79ce4 name
    009038ec  4000009        8                 Bank   0 instance 00a79d2c bank

Note that fields of type Customer and Bank are themselves objects, and you can 
run !DumpObj on them too. You could look at the field directly in memory using
the offset given. "dd a79d40+8 l1" would allow you to look at the bank field 
directly. Be careful about using this to set memory breakpoints, since objects
can move around in the garbage collected heap.

What else can you do with an object? You might run !GCRoot, to determine what 
roots are keeping it alive. Or you can find all objects of that type with 
"!DumpHeap -type Customer".

The column VT contains the value 1 if the field is a valuetype structure, and
0 if the field contains a pointer to another object. For valuetypes, you can 
take the MethodTable pointer in the MT column, and the Value and pass them to 
the command !DumpVC.

The abbreviation !do can be used for brevity.

The arguments in detail:
-nofields:     do not print fields of the object, useful for objects like 
                  String