user1182735
user1182735

Reputation: 764

Binding to list causes memory leak

When I bind an ItemsSource of a ListBox to a List the binding engine holds on to the list elements after the control is gone. This causes all the list elements to stay in memory. The problem goes away when using an ObservalbleCollection. Why does this happen?

The xaml inside the window tag

<Grid>
    <StackPanel>
        <ContentControl Name="ContentControl">
            <ListBox ItemsSource="{Binding List, Mode=TwoWay}" DisplayMemberPath="Name"/>
        </ContentControl>
        <Button Click="Button_Click">GC</Button>
    </StackPanel>
</Grid>

Code behind:

public MainWindow()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }

private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.DataContext = null;
        ContentControl.Content = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

ViewModel

class ViewModel : INotifyPropertyChanged
{
    //Implementation of INotifyPropertyChanged ...

    //Introducing ObservableCollection as type resolves the problem
    private IEnumerable<Person> _list = 
            new List<Person> { new Person { Name = "one" }, new Person { Name = "two" } };

    public IEnumerable<Person> List
    {
        get { return _list; }
        set
        {
            _list = value;
            RaisePropertyChanged("List");
        }
    }

class Person
{
    public string Name { get; set; }
}

Edit: To check the leaking of the person istances, I used ANTS and .Net memory profiler. Both show that after pushing the GC-button only the binding engine is holding reference to the person objects.

Upvotes: 6

Views: 10031

Answers (3)

BionicCode
BionicCode

Reputation: 29028

That's an old post, I see. But the explanations provided, especially by the accepted answer, are not very accurate and the implications are wrong.

Abstract

Beforehand, this is not a real memory leak. The special binding engine's lifetime management for collections that do not implement INotifyCollectionChanged and their associated CollectionView takes proper care of the allocated memory.
WPF supports binding to many different types like DataTable and XML or in general to types that implement IList, IEnumerable or IListSource. If this was a serious bug, then all those bindings would be dangerous.
Microsoft would propagate warnings in their docs against e.g., binding to DataTable like they are doing in case of potential memory leaks in context with events or data binding.

It is indeed true that this special behavior can be avoided by

a) binding to a collection of type INotifyCollectionChanged
b) avoiding creating a CollectionView for a collection that does not implement INotifyCollectionChanged
c) clearing the source collection after use if the collection does not implement INotifyCollectionChanged), so that only the empty collection will reside in memory until the binding engine releases it.

because the observed behavior is actually induced by the actual CollectionView management of the binding engine and not the data binding itself. The binding engine can manage the lifetime of the source collection and the associated ICollectionView more efficiently when it is enabled to listen to the INotifyCollectionCHanged.CollectionChanged event.

The following code triggers the same behavior as would do a binding to a List<T>:

var list = new List<int> {1, 2, 3};
ICollectionView listView = CollectionViewSource.GetDefaultView(list);
list = null;
listView = null;
for (int i = 0; i < 4; i++)
{
  GC.Collect(2, GCCollectionMode.Forced, true);
  GC.WaitForPendingFinalizers();
}

Result: the entire collection reference graph and the CollectionView are still in memory (see explanation below).
This should be proof that the behavior is not introduced by data binding, but by the binding engine's CollectionView management.


Memory Leaks in Context of Data binding

The memory leak issue regarding data binding is not related to the type of the property, but to the notification system that the binding source implements.
The source must either
      a) participate in the dependency property system (by extending DependencyObject and by implementing properties as DependencyProperty) or
      b) implement INotifyPropertyChanged

Otherwise, the binding engine will create a static reference to the source. Static references are root references. Due to their nature to be reachable during the lifetime of the application, such root references, like static fields and every object (memory) they reference, will never be eligible for garbage collection and thus create the actual memory leak.

Collections and CollectionView Management

Collections are a different story. The cause of the alleged leak is not the data binding itself. It's the binding engine that is also responsible for creating the CollectionView of the actual collections.
Whether the CollectionView is created in context of a binding or when calling CollectionViewSource.GetDefaultView: it's the binding engine that creates and manages the views.

The relationship between collection and CollectionView is a unidirectional dependency, where the CollectionView knows the collection in order to synchronize itself, while the collection does not know the CollectionView.

Every existing CollectionView is managed by the ViewManager, which is part of the binding engine. To improve performance, the view manager caches views: it stores them in a ViewTable using WeakReference to allow them to be garbage collected.

When a collection implements INotifyCollectionChanged

           │══════ strong reference R1.1 via event handler ═══════▶│
Collection │                                                       │ CollectionView
           │◀═══  strong reference R1.2 for lifetime management ═══│        ̲                    
                                                                            △                                                                                                                  
                                                                            │
                                                                            │                                 
                                   ViewTable │───── weak reference W1 ──────┘

The CollectionView itself is target of a strong reference R1.1 from the underlying source collection if this collection implements INotifyCollectionChanged.
This strong reference R1.1 is created by the CollectionView the moment it observes the INotifyCollectionChanged.CollectionChanged event (by attaching an event callback that the collection stores in order to invoke it when raising the event).

This way, the lifetime of the CollectionView is coupled to the lifetime of the collection: even if the application has no references to a CollectionView, because of these strong references the lifetime of the CollectionView is extended until the collection itself is eligible for garbage collection.
Since the CollectionView instances are stored in the ViewTable as WeakReference W1, this lifetime coupling prevents the WeakReference W1 from getting garbage collected prematurely.
In other words, this strong coupling R1.1 prevents the CollectionView from being garbage collected before the collection.

Additionally, the manager must also guarantee that as long as the CollectionView is referenced by the application, the underlying collection continues to exist, even if this collection is no longer referenced. This is achieved by keeping a strong reference R1.2 from CollectionView to the source collection.
This reference always exists, no matter the collection type.

When a collection does not implement INotifyCollectionChanged

Collection │◀═══  strong reference R2.1 for lifetime management ════│ CollectionView
                                                                            ̲                                                                             
                                                                            ▲
                                                                            ║
                                                                            ║
                                 ViewTable │════ strong reference R2.2 ═════╝

Now, when the collection does not implement INotifyCollectionChanged, then the required strong reference from collection to CollectionView does not exist (because no event handlers are involved) and the WeakReference stored in the ViewTable to the CollectionView could be potentially garbage collected prematurely.
To fix this, the view manager must keep the CollectionView "artificially" alive.

It does this by storing a strong reference R2.2 to the CollectionView. At this moment the view manager has stored a strong reference R2.2 to the CollectionView (due to the lack of INotifyCollectionChanged) while this CollectionView has a strong reference R2.1 to the underlying collection.
This results in the view manager keeping the CollectionView alive (R2.2) and therefore the CollectionView keeps the underlying collection alive (R2.1): this is the cause for the perceived memory leak.

But this is not a real leak, as the view manager controls the lifetime of the strong reference R2.2 to the CollectionView by registering the strong reference R2.2 with an expiration date. This date is renewed on each access to the CollectionView.

The view manager will now occasionally purge those references when their expiration date is expired. Finally, those references will get collected when the CollectionView is not referenced by the application (ensured by the garbage collector) and the underlying collection is no longer referenced (ensured by the garbage collector).

This behavior is introduced to allow the strong reference R2.2 while avoiding a leak.

Conclusion

Due to the special lifetime management (using expiration dates) for a CollectionView of a collection that does not implement INotifyCollectionChanged, the CollectionView is kept alive (in memory) much longer. And because the CollectionView in general has a strong reference to its source collection, this collection and its items and all reachable references are also kept alive much longer.

If the collection had implemented INotifyCollectionChanged, then the view manager would not have stored the strong reference to the CollectionView and therefore the CollectionView would have been garbage collected the moment it is no longer referenced and the source collection became unreachable.

The important point is, the lifetime of the strong reference to the CollectionView is managed by the ViewManager i.e. binding engine. Due to the management algorithm (the expiration date and the occasional purge), this lifetime is significantly extended.
Therefore, the observation of the persisting allocated memory after all references to the collection and its views have been destroyed is deceiving. It is not a real memory leak.

Tips

"Is there any way to force release view or CollectionView from cache?"

The lifetime management of the CollectionView instances are low-level framework internals that are not controllable from client code.
However, we can control the parameters of this internal algorithm to allow for a significantly shorter lifetime of the CollectionView and its reference graph:

  1. Reuse the collection: reusing the collection means reusing the associated CollectionView. This is a lot cheaper than replacing a collection (and therefore replacing its view).

  2. For data binding to a collection or a CollectionView or when we explicitly create a CollectionView by calling CollectionViewSource.GetDefaultView we must prefer to use a collection that implements INotifyCollectionChanged when possible: as explained in this post, a collection that implements INotifyCollectionChanged allows the binding engine to store the reference to the CollectionView by using a WeakReference. Collections that implement INotifyCollectionChanged are subject to the common garbage collection rules and are not kept alive by the binding engine (ViewManager).
    Remember, the ViewManager has to guarantee that the CollectionView is alive as long as its source collection is alive. The INotifyCollectionChanged.CollectionChanged event allows this, as subscribing to an event always implicitly creates a strong reference to the listener (the ColectionView). If the collection doesn't implement INotifyCollectionChanged, this strong reference has to be created explicitly by the ViewManager (this is what we want to avoid when possible).

  3. If we can't use a collection that implements INotifyCollectionChanged and the collection is very big (in terms of memory) or contains expensive references (in terms of allocated resources), we can at least ensure that:

    • there are no external references to the collection or CollectionView (as this would further extend the lifetime).
      For example, if your custom collection observes the INotifyPropertyChanged.PropertyChanged event of its items, always ensure that the event handlers are removed when the items are removed from the collection. Using a WeakEvenManager implementation like the PropertyChangedEventManager or the generic WeakEvenManager<TSource,TArgs> can improve the lifetime management or fix a related bug.

    • we have cleared the collection e.g. in a IDisposable.Dispose method.
      Clearing the collection at least ensures that the reference graph is freed i.e. eligible for garbage collection, so that only the empty CollectionView resides in memory until the final purge.
      While implementing IDisposable provides a good degree of reliability when the lifetime of the declaring instance is managed by some other objects, we have to make sure we always clear the collection in case we internally replace the instance too:

        // Calling Clear() leaves an empty CollectionView 
        // in memory (pending for GC collection).
        this.MyList.Clear();
      
        // Creating a new instance and binding to it
        // forces the binding engine to create 
        // a new CollectionView for this instance too.
        // However, because we previously cleared the collection,
        // the old items can get garbage collected long time before
        // the old (and now empty) List<T> instance.
        this.MyList = new List<object>();
      
        // Alternatively, reuse the previously
        // cleared collection (recommended)
        this.MyList.AddRange(newItems);
      
  4. And for the sake of completeness: the variable that holds the collection or CollectionView instance must not be a class variable (static variable).

Upvotes: 5

Mr.M
Mr.M

Reputation: 9

I had a look at your example with JustTrace memory profiler and apart from an obvious question why would you kill view model / nullify DataContext and leave view running (in 99.9% of cases you'd kill View and DataContext - hence ViewModel and Bindings go of of scope automatically) here's what I found.

It will work fine if you modify your example to:

  • replace DataContext with new instance of view model, as expected, existing instances of Person go out of scope as MS.Internal.Data.DataBingingEngine flushes all bindings, even they were strong refs not managed by WeakPropertyChangedEventManager , or:
  • ViewModel to replace List with new instance of IEnumerable i.e. new Person[0]/simply null and raise INCP.PropertyChanged("List") on the ViewModel

Above modifications prove you can safely use IEnumerable/IEnumerable in binding. BTW, Person class doesn't need to implement INPC neither - TypeDescriptor binding/Mode=OneTime don't make any difference in this case, I verified that too. BTW, bindings to IEnumerable/IEnumerable/IList are wrapped into EnumerableCollectionView internal class. Unfortunatelly, I didn;t have a chance to go through MS.Internal/System.ComponentModel code to find out why ObservableCollection works when setting DataContext = null, probably because Microsoft guys did a special handing when unsubscribing from CollectionChanged. Feel free to waste few precious lifetime hours on going through MS.Internal/ComponentModel :) Hope It helps

Upvotes: 0

dev hedgehog
dev hedgehog

Reputation: 8801

Ahhh got you. Now I understand what you mean.

You set the Content to null and so you kill the compelte ListBox but still the ItemsSource binds to List and so ListBox memory is not completely released.

That is unfortunately a well known issue and also well documented on MSDN.

If you are not binding to a DependencyProperty or a object that implements INotifyPropertyChanged or ObservableCollection then the binding can leak memory, and you will have to unbind when you are done.

This is because if the object is not a DependencyProperty or does not implement INotifyPropertyChanged or not implementing INotifyCollectionChanged (Normal list is not implementing this) then it uses the ValueChanged event via the PropertyDescriptors AddValueChanged method. This causes the CLR to create a strong reference from the PropertyDescriptor to the object and in most cases the CLR will keep a reference to the PropertyDescriptor in a global table.

Because the binding must continue to listen for changes. This behavior keeps the reference alive between the PropertyDescriptor and the object as the target remains in use. This can cause a memory leak in the object and any object to which the object refers.

The question is...is Person implementing INotifyPropertyChanged?

Upvotes: 7

Related Questions