.Net Framework 和 .Net Core 之间的 WeakReference 行为不同
WeakReference behaves differently between .Net Framework and .Net Core
考虑以下代码:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
#nullable enable
namespace ConsoleApp1
{
class Program
{
static void Main()
{
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(weakRef.IsAlive);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static List<int[]?> makeList()
{
return new List<int[]?> { new int[2] };
}
}
}
- 使用 .Net Framework 4.8 上的发布版或调试版,
代码打印
False
.
- 在 .Net 上发布或调试版本
核心 3.1,该代码打印
True
.
是什么导致了这种行为差异? (这导致我们的一些单元测试失败。)
注意:我将列表初始化放入 makeList()
并关闭内联,试图使 .Net Core 版本与 .Net Framework 版本工作相同,但无济于事。
[编辑] 正如 Hans 指出的那样,添加一个循环可以修复它。
下面的代码将打印False
:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
for (int i = 0; i < 1; ++i)
GC.Collect();
Console.WriteLine(weakRef.IsAlive);
但这会打印出 True
:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();
// Doesn't seem to matter how many GC.Collect() calls you do.
Console.WriteLine(weakRef.IsAlive);
这 是某种奇怪的抖动问题...
仅仅因为某些东西允许被收集并不意味着它有义务尽快被收集。虽然该语言声明允许 GC 确定不再读取局部变量,因此不将其视为根,但这并不意味着您可以 依赖 局部变量的在您上次阅读后立即收集内容。
这不是运行时中已定义行为之间的一些变化,这是两个运行时中的未定义行为,因此它们之间的差异是完全可以接受的。
当我删除列表变量时,我获得了要释放的引用:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static WeakReference CreateWeakReference()
{
return new WeakReference(new List<int[]> { new int[2] });
}
var x = CreateWeakReference();
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
以下测试用例失败:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static List<int[]> CreateList()
{
return new List<int[]> { new int[2] };
}
WeakReference x;
{
var list = CreateList();
x = new WeakReference(list);
list = null;
}
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
如果我们查看 IL,我们可以看到 null 已分配给局部变量 1:
IL_0003: call class [System.Collections]System.Collections.Generic.List`1<int32[]> NUnitTestProject1.Tests::'<TestWeakReferenceWithList>g__CreateList|0_0'()
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: newobj instance void [System.Runtime]System.WeakReference::.ctor(object)
IL_000f: stloc.0
IL_0010: ldnull
IL_0011: stloc.1
IL_0012: nop
考虑以下代码:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
#nullable enable
namespace ConsoleApp1
{
class Program
{
static void Main()
{
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(weakRef.IsAlive);
}
[MethodImpl(MethodImplOptions.NoInlining)]
static List<int[]?> makeList()
{
return new List<int[]?> { new int[2] };
}
}
}
- 使用 .Net Framework 4.8 上的发布版或调试版,
代码打印
False
. - 在 .Net 上发布或调试版本
核心 3.1,该代码打印
True
.
是什么导致了这种行为差异? (这导致我们的一些单元测试失败。)
注意:我将列表初始化放入 makeList()
并关闭内联,试图使 .Net Core 版本与 .Net Framework 版本工作相同,但无济于事。
[编辑] 正如 Hans 指出的那样,添加一个循环可以修复它。
下面的代码将打印False
:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
for (int i = 0; i < 1; ++i)
GC.Collect();
Console.WriteLine(weakRef.IsAlive);
但这会打印出 True
:
var list = makeList();
var weakRef = new WeakReference(list[0]);
list[0] = null;
GC.Collect();
GC.Collect();
GC.Collect();
GC.Collect();
// Doesn't seem to matter how many GC.Collect() calls you do.
Console.WriteLine(weakRef.IsAlive);
这 是某种奇怪的抖动问题...
仅仅因为某些东西允许被收集并不意味着它有义务尽快被收集。虽然该语言声明允许 GC 确定不再读取局部变量,因此不将其视为根,但这并不意味着您可以 依赖 局部变量的在您上次阅读后立即收集内容。
这不是运行时中已定义行为之间的一些变化,这是两个运行时中的未定义行为,因此它们之间的差异是完全可以接受的。
当我删除列表变量时,我获得了要释放的引用:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static WeakReference CreateWeakReference()
{
return new WeakReference(new List<int[]> { new int[2] });
}
var x = CreateWeakReference();
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
以下测试用例失败:
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace NUnitTestProject1
{
public class Tests
{
[TestCase(2, GCCollectionMode.Forced, true)]
public void TestWeakReferenceWithList(int generation, GCCollectionMode forced, bool blocking)
{
static List<int[]> CreateList()
{
return new List<int[]> { new int[2] };
}
WeakReference x;
{
var list = CreateList();
x = new WeakReference(list);
list = null;
}
Assert.IsTrue(x.IsAlive);
GC.Collect(generation, forced, blocking);
Assert.IsFalse(x.IsAlive);
}
}
}
如果我们查看 IL,我们可以看到 null 已分配给局部变量 1:
IL_0003: call class [System.Collections]System.Collections.Generic.List`1<int32[]> NUnitTestProject1.Tests::'<TestWeakReferenceWithList>g__CreateList|0_0'()
IL_0008: stloc.1
IL_0009: ldloc.1
IL_000a: newobj instance void [System.Runtime]System.WeakReference::.ctor(object)
IL_000f: stloc.0
IL_0010: ldnull
IL_0011: stloc.1
IL_0012: nop