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 ConditionalWeakTables) 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

Most upvoted comments

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:

table.Add(key, new Tuple<object, byte[]>(table, new byte[1000000]));

to

table.Add(key, new Tuple<object, byte[]>(null, new byte[1000000]));

(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 the ConditionalWeakTable 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 the ConditionalWeakTable in .NET: It allows to store key-value-pairs, where an entry in the WeakMap doesn’t prevent the key from being garbage-collected (even if the value has a reference to the key):

If an object that is being used as the key of a WeakMap key/value pair is only reachable by following a chain of references that start within that WeakMap, then that key/value pair is inaccessible and is automatically removed from the WeakMap.

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):

let key = new Object();

while (true) {
    let map = new WeakMap();
    map.set(key, [map, new ArrayBuffer(1000000)]);
}

Now, because the WeakMap in ECMAScript has a similar concept like the ConditionalWeakTable 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. image

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 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!

@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.