runtime: ConditionalWeakTable causes a memory leak if one of their values references the table
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:
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<object, Tuple<object, byte[]>>();
table.Add(key, new Tuple<object, byte[]>(table, new byte[1000000]));
GC.Collect();
}
}
}
}
Expected behavior: The memory consumption of the program should stay in the same area, because when a new ConditionalWeakTable
instance is created, there are no more references to the previous ConditionalWeakTable
and its Tuple
value, so they should be able to be reclaimed by the Garbage Collector.
Actual behavior: The memory consumption rises rapidly (4 GB after some seconds) until an OutOfMemoryException
is thrown, as the byte arrays are not reclaimed by the garbage collector.
However, if you remove the reference to the table by replacing table.Add(...)
with table.Add(key, new Tuple<object, byte[]>(null, new byte[1000000]))
, the problem disappears.
If the algorithm cannot be implemented such that it can detect that there are no more references to the table and its values, I think the ConditionalWeakTable
should implement a Dispose()
method that allows to clear all key-value-pairs.
The behavior is the same for .NET Core (.NETCoreAPP 1.1) and .NET Framework 4.6.2.
Thanks!
About this issue
- Original URL
- State: open
- Created 7 years ago
- Reactions: 2
- Comments: 36 (19 by maintainers)
Commits related to this issue
- [crossgen2] Fix memory leak in _corInfoImpls. _corInfoImpls elements keeps _compilation reference wich keeps reference to whole table _corInfoImpls. The circular referene together with issue #12255 p... — committed to t-mustafin/runtime by t-mustafin 3 years ago
- [crossgen2] Implement Dispose in Compilation. _corInfoImpls elements keeps _compilation reference which keeps reference to whole table _corInfoImpls. The circular referene together with issue #12255 ... — committed to t-mustafin/runtime by t-mustafin 3 years ago
- [crossgen2] Fix memory leak in _corInfoImpls. (#49764) * [crossgen2] Implement Dispose in Compilation. _corInfoImpls elements keeps _compilation reference which keeps reference to whole table _cor... — committed to dotnet/runtime by t-mustafin 3 years ago
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:
to
(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. But as this behavior only occurs in a special circumstance (when the value contains a reference to the
ConditionalWeakTable
in which the value is stored), it seems like a bug to me.Additionally, from reading the documentation of
ConditionalWeakTable
, I don’t read it to mean that values are attached to keys permanently and are therefore not GCed even if theConditionalWeakTable
is CGed. Rather, it should only look like values being attached to keys, while internally they are stored in a separate dictionary/table to which the keys don’t have any relation/reference.Note, that ECMAScript (JavaScript) defines a
WeakMap
that has a similar concept like theConditionalWeakTable
in .NET: It allows to store key-value-pairs, where an entry in theWeakMap
doesn’t prevent the key from being garbage-collected (even if the value has a reference to the key):However, if run the same test in ECMAScript implementations like SpiderMonkey (Mozilla Firefox) and V8 (Google Chrome, also used in Node.js), a leak doesn’t occur (except for Chakra (Microsoft Edge) which funnily seems to have exactly the same behavior as in .NET where a leak only occurs if the value has a reference to the
WeakMap
):Now, because the
WeakMap
in ECMAScript has a similar concept like theConditionalWeakTable
in .NET, it is used e.g. by Jurassic, a JavaScript Engine for .NET, to implement the WeakMap. Unfortunately, this means when running the above JavaScript code in Jurassic, a memory leak will also occur here.Thanks!
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.
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.
Meet the same problem of @zipswich ,but have no idea to fix it. Who can provide a solution?
Hi all, I have the same behavior of zipswich and I can provide a repro project as well. Regards, Damiano
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 expectConditionalWeakTable
to implementIDisposable
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!
@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]));
.@kornelpal I believe you made the same mistake above: the value will keep key alive, making it no longer a weak dictionary.
I think this really cannot be done with
DependentHandle
, given that it only has one source. To make it possible, you need at least two, i.e., only when both soucres are alive, make the target alive. With a two-source implementation, one can easily chain and make three, four, etc., but you really can’t start from one.