Reputation: 764
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
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.
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.
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.
CollectionView
ManagementCollections 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.
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.
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.
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.
"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:
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).
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).
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);
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
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:
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
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