如果一个易失性引用在线程加载它和调用它的函数之间发生了变化,那么旧的对象可以被垃圾收集吗?

本文关键字:对象 变化 之间 一个 易失性 引用 函数 如果 调用 线程 加载 | 更新日期: 2023-09-27 17:51:06

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

static volatile Something foo;
void update() {
    newFoo = new Something();
    foo = newFoo;
}
void invoke() {
    foo.Bar();
}

线程A执行update,线程B执行invoke。这两个线程有定时,使得invoke加载foo的地址,update覆盖foo,然后在Bar被调用之前进行垃圾收集。

是否有可能垃圾收集可能会收集foo引用的旧对象,导致Bar在一些已收集的内存上被调用?

注意这个问题主要是出于好奇。我也愿意接受更好的标题

如果一个易失性引用在线程加载它和调用它的函数之间发生了变化,那么旧的对象可以被垃圾收集吗?

垃圾收集器将暂停所有正在运行的线程的状态,以解决围绕由此进行的任何内存访问的任何竞争条件。静态变量foo是否稳定与否,垃圾收集器将知道任何对象的身份,叫Bar可能被调用,并将确保任何此类对象对象将继续存在,只要有任何执行路径通过的任何正常或"隐藏"字段可能被访问,通过电话KeepAlive可能被执行,或者通过它可能reference-compared到另一个参考。

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

有两件事:

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

两个线程有定时,这样调用加载foo的地址,

这就是你的答案。当foo的旧值在堆栈上时(准备调用. bar()),它被认为是根引用。它将成为(已经是)Bar中的this引用,并且实例可以在不再需要时立即收集。这可能发生在Bar()执行期间。

内存安全在这里从来没有风险。

不,它不会在"已收集内存"上被调用。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 -在对象上调用后绑定方法,将返回值压入计算堆栈

两个线程有定时,这样调用加载foo的地址,更新覆盖foo。

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

如果字段不是volatile,那么foo将保存对前一个值的引用,使GC无法收集它,因此Bar()将在旧引用上调用,而不是在"可能被收集的一些内存"上调用。. net中的内存管理就是用来处理这种情况的,所以不会执行一些任意的不安全的内存地址。