如果 volatile 引用在线程加载引用和对其调用函数之间发生了变化,旧的 object 是否可以被垃圾收集?

If a volatile reference has changed between a thread loading the reference and calling a function on it, can the old object be garbage collected?

我有两个线程执行下面的代码:

static volatile Something foo;

void update() {
    newFoo = new Something();
    foo = newFoo;
}

void invoke() {
    foo.Bar();
}

线程 A 执行 update,线程 B 执行 invoke。这两个线程的时间安排使得 invoke 加载 foo 的地址,update 覆盖 foo,然后垃圾 collection 发生在 Bar 之前召入。

垃圾 collection 是否可能收集 foo 引用的旧 object,导致 Bar 在已收集的某些内存上被调用?

请注意,这个问题主要是出于好奇。我也对更好的标题持开放态度。

The two threads have timing such that invoke loads the address of foo, update overwrites foo.

因为您的静态字段被标记为 volatile,运行时保证对给定字段的任何更改都将立即更新,并且使用它的任何线程都将具有最外层的更新值。因此目前的场景是不可能的。

如果该字段不是 volatile,那么 foo 将持有对先前值的引用,使 GC 无法收集它,因此 Bar() 将在旧引用上调用,而不是在 "some memory that might be collected" 上调用。 .NET中的内存管理就是为了处理这种情况,所以不会执行一些任意的不安全内存地址。

不,它不会在 "collected memory" 上调用。 Bar() 方法在旧对象或新创建的对象上调用。这取决于它们中的哪一个首先加载到堆栈中。 (我不认为堆栈是垃圾回收的)

反编译后的代码很清楚:

.method private hidebysig static 
    void update () cil managed 
{
    // Method begins at RVA 0x2170
    // Code size 16 (0x10)
    .maxstack 1
    .locals init (
        [0] class ConsoleApplication1.Something newFoo
    )

    IL_0000: nop
    IL_0001: newobj instance void ConsoleApplication1.Something::.ctor()
    IL_0006: stloc.0
    IL_0007: ldloc.0
    IL_0008: volatile.
    IL_000a: stsfld class ConsoleApplication1.Something modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)  ConsoleApplication1.Program::foo
    IL_000f: ret
} // end of method Program::update


.method private hidebysig static 
    void invoke () cil managed 
{
    // Method begins at RVA 0x218c
    // Code size 15 (0xf)
    .maxstack 8

    IL_0000: nop
    IL_0001: volatile.
    IL_0003: ldsfld class ConsoleApplication1.Something modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile)  ConsoleApplication1.Program::foo
    IL_0008: callvirt instance class ConsoleApplication1.Something ConsoleApplication1.Something::Bar()
    IL_000d: pop
    IL_000e: ret
} // end of method Program::invoke

stsfld - 用计算堆栈中的值替换静态字段的值。

ldsfld - 将静态字段的值推送到计算堆栈上。

callvirt - 在对象上调用后期绑定方法,将 return 值推入计算堆栈。

这有两点:

  • GC在进行回收时,从所谓的"roots"开始判断是否引用了一个对象实例。根可以是存储在堆栈上的局部变量,甚至可以是保存对象引用的寄存器。当您的代码调用 Bar() 时,代码可能会将实例地址加载到寄存器中(几年前是 ECX,我现在不确定)。它将作为 "this" 传递给方法。如果发生垃圾回收,ECX 将被视为根,实例将不会被标记为垃圾。
  • GC 不会随机发生。在 GC 发生之前,线程会在指定的 "safe points" 处停止,因此程序将处于良好且一致的垃圾收集状态。它有助于避免您描述的情况。

垃圾收集器将暂停所有 运行 线程的状态足够长的时间,以解决围绕由此产生的任何内存访问的任何竞争条件。无论静态变量 foo 是否易变,垃圾收集器都会知道可能调用 Bar 的任何对象的身份,并将确保任何此类对象对象将继续存在只要有任何执行路径,通过它可以访问任何正常或 "hidden" 字段,通过它可以对其执行 KeepAlive 调用,或者通过它可以对它进行引用比较另一个参考。

系统可能在某些情况下调用 Finalize 一个对象,同时存在对它的可观察引用,但系统保持绝对不变,即 GC 知道所有可以使用的引用通过上述方式的任何执行路径;只要任何此类引用存在,对象就保证存在。

The two threads have timing such that invoke loads the address of foo,

仅此一项就可以为您提供答案。当 foo 的旧值在堆栈上时(准备调用 .Bar()),它被认为是根引用。它将成为(已经是) Bar 内的 this 引用,并且可以在不再需要时立即收集该实例。那可能是在 Bar() 的执行期间。

内存安全在这里永远不会受到威胁。