Here is some simple C# .NET 7 code that causes a memory leak:
using System.Threading;
while (true)
{
ThreadLocal<object> tl = new();
tl.Value = tl;
}
If you run this code, the memory usage of the process increases very rapidly (on my computer, a gigabyte every couple seconds) until it runs out of memory and crashes.
Setting tl.Value = new object();
instead fixes the memory leak. So does adding a call to tl.Dispose();
at the end of the loop, but since ThreadLocal has a finalizer that itself calls Dispose, I would have expected Dispose to (eventually) get called regardless.
The above code is of course a contrived example, but the memory leak also occurs if the ThreadLocal only references itself indirectly through whatever object it directly references. As long as there is a reference chain that reaches back to the ThreadLocal, the memory leak occurs. (This is how I originally discovered the leak.)
Is this expected/documented behavior? Why does it happen?
EDIT: Here is a version of the above program that manually invokes the garbage collector every 100000 loop iterations. Note that even with the manually GC of all generations, it still leaks memory. (Albeit somewhat less quickly, presumably due to the amount of time spent GC'ing.)
using System;
using System.Threading;
int countUntilGc = 100000;
while (true)
{
ThreadLocal<object> tl = new();
tl.Value = tl;
countUntilGc--;
if (countUntilGc <= 0)
{
GC.Collect();
countUntilGc = 100000;
}
}