内存消耗代码优化,垃圾收集器理论

4
在我的WPF应用程序中,我通过以下方式调用新窗口:
_newWin = new WinWorkers_AddWorker();
_newWin.WindowState = this.WindowState;
_newWin.Show();

其中_newWin是一个私有窗口对象。

我的问题:

  1. 在调用_newWin.Show()后,我应该将_newWin赋值为null吗?

  2. 这样做会减少内存消耗,因为垃圾收集器/析构函数会更早地清理null值对象吗?

3个回答

6

通常将值设为null是无关紧要的,非常少有用处,偶尔会有害。

首先,让我们考虑最简单的情况:

private void DoStuff()
{
  var newWin = new WinWorkers_AddWorker();
  newWin.WindowState = this.WindowState;
  newWin.Show();
  int irrelevant = 42;
  this.whoCares = irrelevant * 7;
  int notRelevantEither = irrelevant + 1;
  this.stillDontCare = notRelevantEither * irrelevant;
}

在这里,newWin仅存在于该方法中;它在其中创建,并且通过返回或分配给具有更广泛范围的成员而未离开该方法的范围。
当许多人询问newWin何时被垃圾回收时,他们会告诉你,在带有this.stillDontCare的行之后,因为那时newWin超出了范围。因此,我们可以在最后一次使用后将newWin = null赋值,但这可能是微不足道的。
从概念上讲,这是正确的,因为我们可以添加处理newWin的代码,直到那个点,并且newWin可供我们使用。
实际上,很可能newWin.Show()之后立即变得适合进行收集。虽然在概念上它在此之后仍在作用域内,但实际上它没有被使用,编译器知道这一点。(从现在开始,“编译器”将指生成实际运行代码的整个过程,包括IL编译器和JIT)。由于不再使用newWin本身使用的内存(即堆栈上的引用,而不是对象),编译器可以将该内存用于irrelevant或其他内容。没有活动引用,对象适合进行收集。
实际上,如果在一个对象上调用的最后几个方法实际上不使用this指针(直接或通过使用成员字段),则对象甚至可以在调用这些方法之前被回收,因为它们实际上不使用该对象。如果有一个方法其this指针从未被使用过(再次,直接或间接地),那么它可能永远不会被创建!
现在,考虑到这一点,我们可以看到,如果变量在变量超出范围之前赋值为空,它实际上并不会使微不足道的差异。
实际上,这种赋值甚至可能使其变得更难以获得资格,因为如果编译器无法看到变量的使用不会影响对象(不太可能,但如果存在try...catch...finally块使分析更加复杂,则可能发生),那么它甚至可能延迟认定对象符合条件的时间点。这也可能是微不足道的,但它确实存在。
到目前为止都很简单;如果我们不管它,好事情就会发生,并且让它自然而然地留下来很容易。
然而,将引用设置为null可能会有所好处。考虑以下内容:
public class SomeClass
{
  private WorkerThing _newWin;
  private void DoStuff()
  {
    _newWin = new WinWorkers_AddWorker();
    _newWin.WindowState = this.WindowState;
    _newWin.Show();
  }
}

考虑到这里,在调用DoStuff()后,_newWin被存储在成员变量中。它不会在SomeClass实例超出范围之前失效。那什么时候会发生呢?
好吧,我不能回答这个问题,但有时答案很重要。如果SomeClass本身也是短暂的,那就无所谓了。它很快就会失效,带着_newWin一起失效。然而,如果我们将_newWin = null赋值,则该对象将立即有资格进行收集。
现在,这里有一些重要的注意事项:
  1. 首先,没有充分的理由使_newWin成为成员变量。如果上面的示例是完整的代码,我们将把它移回到DoStuff()中,并且不仅在效率方面获得收益,而且在正确性方面获得更多收益,因为我们不能从另一个成员对_newWin做出愚蠢的操作。
  2. 如果我们将某些东西保存在成员变量中,那么可能有很好的理由。这个好理由将优先于尽快清除变量。
  3. 大多数对象本身并不占用太多内存。偶尔出现一个成员变量也不会有什么影响。
因此,将null分配给成员变量的主要原因是因为null已成为最适当的值。将null分配给不再使用的成员通常不是为了尽快释放其内存,而是因为不再适合使用,当它为null时,这一点变得不可能,并且向代码的其余部分清楚地发出信号。
如果引用的生命周期比方法长(因此放在成员变量中),并且比包含对象的生命周期明显短,并且消耗非常大的内存,那么将null赋值开始变得有意义。在极为罕见的情况下,当这种组合发生时,我们可能想将其分配为null,以表明它不再可供类使用,因此我们仍然不会将null分配为释放到GC的目的。这几乎是不可能的,但真的“不行”。

2
垃圾回收不会清除空对象。如果您将引用设置为null,则只是删除了指向对象的引用,从而实际上降低其保留计数器。
但是,当一个对象离开作用域且无法被代码收回时,这个计数器也会减少。所以你试图做的是没用的。
无论如何GC都会选择在没有引用它的情况下释放它,但如果显示该窗口,则可以确信它在某个地方仍然被引用。 编辑:如评论中所述,也许引用计数不是.NET虚拟机所采用的方式(对不起,我不使用M$平台),但原则仍然相同。无论如何,由于它是可见的,因此您的窗口将不会被GC回收。

1
注意数计数并不是实现垃圾收集的真正方式。据我所知,它采用的是标记-清除或类似的方法。 - Dykam
@Dykam 是正确的,这是标记和清除。事实上,它可以在超出范围之前进行收集。我将添加一个详细的答案。 - Jon Hanna
Mono 也不是基于引用计数的。 - Jon Hanna
啊,是的,您说的关于作用域和引用计数的观点适用于某些语言,如VB6、PHP和Python。但对于.NET或Java来说并不正确。 - Jon Hanna

0

它不会删除对象,因为它将在应用程序的其他位置(在内部代码中)被引用。否则,将 _newWin 的值设置为 null 并进行垃圾回收将使您的窗口消失(并可能导致程序崩溃),而这种情况是不会发生的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接