如果一个易失性引用在线程加载它和调用它的函数之间发生了变化,那么旧的对象可以被垃圾收集吗?
本文关键字:对象 变化 之间 一个 易失性 引用 函数 如果 调用 线程 加载 | 更新日期: 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中的内存管理就是用来处理这种情况的,所以不会执行一些任意的不安全的内存地址。