IdentityWeakReference should be IDisposable
See original GitHub issueIdentityWeakReference inherits from WeakReference, which has a finalizer, but its finalizer is especially handled by CLR to be ignored:
https://referencesource.microsoft.com/#mscorlib/system/weakreference.cs,108
When you inherit the WeakReference CLR doesn’t ignore the finalizer anymore and the object become finalizable. IdentityWeakReference doesn’t supress finalization causing a huge pressure on finalizer thread.
In one of my traces I found a 5 seconds GC pause due to that, the pause was not in GC itself, but in the suspension phase before starting the GC. I tracked what the SuspendEE was waiting, and it was the finalizer thread. When investigating why finalizer was hanging, I found this:
IdentityWeakReference should implement IDisposable and call GC.SuppressFinalize(this) in Dispose method. And all instances of IdentityWeakReference needs to call Dispose before losing the reference, for example:
You can easily simulate the issue using this code and uncomment the lines to fix it:
static object x = new object();
static async Task Main(string[] args)
{
const int count = 1000000;
var weaks = new List<IdentityWeakReference<object>>(count);
for (int i = 0; i < count; i++)
{
weaks.Add(new IdentityWeakReference<object>(x));
}
Console.WriteLine("Start Perfview collection and press Enter");
Console.ReadLine();
GC.KeepAlive(weaks);
for (int i = 0; i < count; i++)
{
//weaks[i].Dispose();
}
weaks = null;
Stopwatch c = Stopwatch.StartNew();
GC.Collect(2, GCCollectionMode.Default, true, true);
GC.WaitForPendingFinalizers();
GC.Collect(2, GCCollectionMode.Default, true, true);
GC.WaitForPendingFinalizers();
Console.WriteLine("GC DONE. Stop Perfview");
Console.WriteLine($"{c.ElapsedMilliseconds}ms");
Console.ReadLine();
}
internal class IdentityWeakReference<T> : WeakReference
//, IDisposable
where T : class
{
private readonly int hash;
private static readonly object NULL = new object();
//private bool disposedValue;
public IdentityWeakReference(T target)
: base(target ?? NULL)
{
hash = RuntimeHelpers.GetHashCode(target);
}
public override int GetHashCode()
{
return hash;
}
public override bool Equals(object o)
{
if (ReferenceEquals(this, o))
{
return true;
}
if (o is IdentityWeakReference<T> iwr && ReferenceEquals(this.Target, iwr.Target))
{
return true;
}
return false;
}
public new T Target
{ // note: if this.NULL is the target, it will not cast to T, so the "as" will return null as we would expect.
get => base.Target as T;
set => base.Target = value;
}
//protected virtual void Dispose(bool disposing)
//{
// if (!disposedValue)
// {
// if (disposing)
// {
// // TODO: dispose managed state (managed objects)
// }
// // TODO: free unmanaged resources (unmanaged objects) and override finalizer
// // TODO: set large fields to null
// disposedValue = true;
// }
//}
//public void Dispose()
//{
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
// Dispose(disposing: true);
// GC.SuppressFinalize(this);
//}
}
GC Stats without the fix:
And even this application doing nothing. GC takes 5 seconds (technically not GC, but Finalizer):
GC Stats with the fix:
No Finalized Objects
GC is almost instantaneous, as expected
Issue Analytics
- State:
- Created 2 years ago
- Comments:10 (7 by maintainers)
I have submitted #509 with a patch to fix the issues with using the wrong collection type.
ConditionalWeakTable<TKey, TValue>
WeakDictionary<TKey, TValue>
WeakKey<T>
class (used byWeakDictionary<TKey, TValue>
) was modified to be backed byWeakReference<T>
rather thanWeakReference
WeakDictionary<TKey, TValue>
was modified to be backed byConcurrentDictionary<TKey, TValue>
to allow the reap to happen while doing a forward iterationYou can download a CI build of the changes from here, and I would appreciate your help testing them to ensure the problem is resolved.
Being that we switched to a wrapped (rather than inherited) instance, I suspect the finalizer issue has been sidestepped. However, if there is still an issue with it, please let us know.
Do note that because of the conditional compilation branching it would be helpful to know which .NET platform you are targeting going forward so we know which implementation you are referring to.
@NightOwl888 do you have any ETA when next release will be done?