Hi,
I found an odd behavior of System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>
in both .NET Core and .NET Framework which looks like a bug to me: If you create multiple instances of the ConditionalWeakTable
and store a key-value pairs in them, where the key stays alive and the value contains a reference to the ConditionalWeakTable
, the values are not garbage-collected after they (and the ConditionalWeakTable
s) are no longer referenced.
For example, create a .NET Core Console application with the following code:
```c#
using System;
using System.Runtime.CompilerServices;
namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
object key = new object();
while (true) {
var table = new ConditionalWeakTable
table.Add(key, new Tuple
cc: @Maoni0
You are inducing gen2 GCs, so GC doesn't participate in deciding what's live and what's dead - it's purely determined by the user code. If you use the !gcroot sos command, it should shed some light as to what's holding an object alive.
Hi @Maoni0,
sorry, I'm not sure I understand you correctly. I included the GC.Collect()
call just to show that even after an explicit GC the objects are not collected, but the behavior is the same when you remove this line.
You can reproduce the leak by running the above code as a .NET Core Console Application (VS target ".NETCoreApp 1.1") on Windows: After some seconds, the programm will crash.
Thanks!
In a blocking gen2 GC, GC does not participate in determining object's lifetime at all. So if a blocking gen2 does not collect something, it means that something is held alive by user code. The fact that you keep doing blocking gen2's and the object is not going away, it simply means GC is being told that it's alive. Does this make sense? I was trying to help you to find out who's holding it live. You can use !gcroot (I think gcroot does look at the dependent handles); !gchandles will show you what handles there are and what objects they hold onto.
@maonis There is no user (ie non-framework) code keeping anything alive in the example above. The ConditionalWeakTable is keeping itself alive because of how it is implemented using conditional handles.
@kpreisser You should be able to workaround the issue by storing the back-references to ConditionalWeakTable in another weak reference, like table.Add(key, new Tuple<object, byte[]>(new WeakReference(table), new byte[1000000]));
.
@jkotas by user code I just meant not GC code...
Hi @Maoni0,
Yes, it makes sense, thank you. But note: In my report I did not want to say that there's a problem with GC (in that it would not collect objects to which no more references exist) - I think GC is working correctly. Rather, it is the ConditionalWeakTable
which somehow keeps references to the value objects alive even if no more non-framework code contains references to them and to the table, thus preventing GC from collecting the objects. This is what I wanted to show in the report.
I will try to see if I can run the commands which you suggested, thanks!
Hi @jkotas,
thank you. The above example code is only a simplified reproduction which I observed in a more complex application (although in .NET Framework, but the behavior is the same as on .NET Core on Windows), where the value objects need a reference to the table in order to get access to other attached objects.
Until now, I worked-around the issue by calling the internal ConditionalWeakTable.Clear()
method using reflection, but with using your tip I was able to resolve the issue by clearing-out the references to the ConditionalWeakTable
in the value objects after I no longer need the table, which is cleaner than using reflection. (Simply using a WeakReference
to store the table would not have worked in my case, as I would need to ensure the table is not collected until I no longer need it, so I would have to store a strong reference to it on some other place).
Thanks!
I worked-around the issue by calling the internal
ConditionalWeakTable.Clear()
method using reflection,
BTW: The Clear method was added as public API in .NET Core 2.0.
@kpreisser I don't see this as a bug. I think it works exactly as expected. The whole purpose of ConditionalWeakTable
is to tie the life time of the "right" part of the table to the life time of its "left" part. You effectively tied your table
to your key
, and since the object referenced by key
is still alive - so is table
.
The purpose of ConditionalWeakTable
is to add an 'extension' field to an arbitrary object that would work the same way normal fields work. So in your case, if your key
simply had a normal field referencing your table
- you would observe the same 'leak'.
What you are doing - you are basically relying on the fact that when ConditionalWeakTable
is collected - it will remove all the stored key-value associations and thus would allow the collection of the 'values'. While it's logical to assume that, I wouldn't rely on it. I don't think it was intended to be used in this way. It's not even documented.
Hi @Torvin,
thanks for your reply!
However, I'm not sure if I follow: If it is expected that the value (right part) is tied to the life time of the key (left part), then I would have expected that the leak would also occur even if the value doesn't reference the ConditionalWeakTable
, because the value would still reference the byte array and the key is still reachable.
However, if you change the line in the above code:
```c#
table.Add(key, new Tuple
when you do this:
table.Add(key, new Tuple<object, byte[]>(table, new byte[1000000]));
a GC handle of the dependent type is created with its primary value as key and secondary value as the tuple obj. and this GC handle needs to be cleaned up by something - in the CWT case it's either by the finalizer or setting the primary of this handle to null (if you remove an entry). if you don't do either it'll stay and hold the secondary object live which means table will be alive. key can still be alive but if the handle is freed or if it's cleared as the primary for the GC handle it will not hold the secondary (tuple obj) alive.
@kpreisser
However, if you change the line in the above code (so that the value doesn't contain a reference to the table), then the leak doesn't occur, meaning that the values are garbage-collected even though the key is still alive.
What I'm saying is - this behaviour (when collection of CWT also removes the GC handles) isn't documented and if I were you I wouldn't rely on it.
Hi @Torvin (sorry for the late reply),
OK, so this would mean that I need to clear the ConditionalWeakTable
once I don't need it any more.
But this requires using reflection for .NET Framework (up to 4.7.2) since the .Clear()
method is not public there (while for .NET Core it is). Also, as an API consumer, I would expect ConditionalWeakTable
to implement IDisposable
in such a case.
Otherwise, I don't think it is unreasonable to expect that GCing the table will also GC the handles since this is an implementation detail and the documentation doesn't indicate that special handling is required.
Thanks!
@jkotas @Maoni0 we could fix this in relatively simple way by creating a new struct to replace CWT usage of DependsHandle
as the holder for key and value . This alternative struct could have a weak GCHandle to the key and a reference to the value. This way the change to the code will be very localized. At minimum some perf tests needs to be added to see how perf & mem will be affected, besides that, any concerns with this approach? Any gotchas that I should be aware?
This alternative struct could have a weak GCHandle to the key and a reference to the value.
I do not think you would be able to maintain the CWT contract with this structure. It would be keeping the value
alive for much longer (potentially infinitely long) once the entry becomes dead.
Ops, you're right: while the CWT is not collected values will still be alive. Since it is a very particular case it seems better to document the behavior and close the issue.
I think it makes sense for this code to leak memory under the following view: CWT is meant to act as if it attached additional fields to existing objects. So key
behaves as if it had additional fields. key
is alive and it's "field" references the value and the table. So those are kept alive as well.
I wonder if a memory leak of a UWP app is related to this. Whenever I open and close a page of a production app, ConditionalWeakTable count increases by 1k+. This is very consistent. I cannot find any instance of the page class in memory snapshots after it is closed, so I assume the page is disposed correctly.
I created a repo with a few blank pages. Navigating among the pages keeps increasing ConditionalWeakTable count in memory snapshots.
Hi all,
I have the same behavior of zipswich and I can provide a repro project as well.
Regards, Damiano
Meet the same problem of @zipswich ,but have no idea to fix it.
Who can provide a solution?
Most helpful comment
I wonder if a memory leak of a UWP app is related to this. Whenever I open and close a page of a production app, ConditionalWeakTable count increases by 1k+. This is very consistent. I cannot find any instance of the page class in memory snapshots after it is closed, so I assume the page is disposed correctly.
I created a repo with a few blank pages. Navigating among the pages keeps increasing ConditionalWeakTable count in memory snapshots.