【问题标题】:Variable lifetime可变寿命
【发布时间】:2015-09-17 04:30:55
【问题描述】:

当执行行超出代码块时,变量会发生什么? 例如:

1  public void myMethod()
2  {
3     int number;
4     number = 5;
5  }

所以,我们声明并设置变量。当它超出代码块(第 5 行)时,变量号会发生什么?

这是另一个创建类实例的示例:

7   public void myMethod()
8   {
9      Customer myClient;
10     myClient = new Customer();
11  }

当它超出代码块(第 11 行)时,对象引用 myClient 会发生什么?

我猜在这两种情况下变量都被分配了,但是什么时候被释放了?

【问题讨论】:

  • 我敢打赌编译器甚至不会费心在第一种方法中做任何事情。第二种方法必须调用构造函数,但实例不会存储在任何地方,因为它没有被使用。
  • 对于初学者(我是)程序员来说,垃圾收集不是初学者的主题。因此我的问题是合法的,因为初学者不会遇到垃圾收集主题。我以一种初学者可以理解的方式构建问题,因为我们大多听到“它不在范围内”,这并不能完全说明发生了什么。
  • 主要问题是你不应该真正关心发生了什么——这是语言的“管理”部分。您只需要在开始进行性能调整时才真正需要关心,而这确实不是初学者的主题。但请注意,如果您的 C# 书籍/教程/示例告诉您在变量离开作用域时释放了内存,那是错误的(或至少具有误导性)。作用域的主要目的是简化编程 - 您根本无法再访问本地,因此您不必一直在范围之外考虑它。
  • 我不认为这是完全重复的,它询问了 GC 部分以外的位(尽管这也是相关的)。

标签: c# lifetime object-lifetime lifetime-scoping


【解决方案1】:

作为一个变量,它是 C# 语言中的一个概念。它在代码块之外没有“发生”,因为它在代码块内。在这句话之外,word这个词什么也没有发生。

当然,您的意思是当代码运行时变量会发生什么变化,但值得记住区别,因为在考虑这个问题时,我们正在转移到变量与 C# 中不同的级别。

在这两种情况下,代码都会转换为 CIL,然后在运行时转换为机器代码。

CIL 可能有很大不同。例如,这是第一个在调试模式下编译时的样子:

.method public hidebysig instance void myMethod () cil managed 
{
  .locals init ([0] int32) // Set-up space for a 32-bit value to be stored
  nop                      // Do nothing
  ldc.i4.5                 // Push the number 5 onto the stack
  stloc.0                  // Store the number 5 in the first slot of locals
  ret                      // Return
}

以下是编译发布时的外观:

.method public hidebysig instance void myMethod () cil managed 
{
  ret                      // Return
}

由于未使用该值,编译器将其作为无用的垃圾删除,并仅编译一个立即返回的方法。

如果编译器没有删除这样的代码,我们可能会期待:

.method public hidebysig instance void myMethod () cil managed 
{
  ldc.i4.5                 // Push the number 5 onto the stack
  pop                      // Remove value from stack
  ret                      // Return
}

Debug 构建存储时间更长,因为检查它们对调试很有用。

当发布版本确实将内容存储在本地数组中时,它们也更有可能重用方法中的槽。

然后将其转换为机器代码。它的工作方式类似于它会产生数字 5,将其存储在本地(在堆栈或寄存器中),然后再次删除它,或者因为未使用的变量已被删除,所以什么也不做. (甚至可能不执行该方法;该方法可以被内联,然后由于它不执行任何操作而被完全删除)。

对于带有构造函数的类型,还有一些事情要做:

.method public hidebysig instance void myMethod () cil managed 
{
  .locals init ([0] class Temp.Program/Customer)       // Set-up space for a reference to a Customer

  nop                                                  // Do nothing.
  newobj instance void SomeNamespace/Customer::.ctor() // Call Customer constructor (results in Customer on the stack)
  stloc.0                                              // Store the customer in the frist slot in locals
  ret                                                  // Return
}

.method public hidebysig instance void myMethod () cil managed 
{
  newobj instance void SomeNamespace/Customer::.ctor() // Call Customer constructor (results in Customer on the stack)
  pop                                                  // Remove value from stack
  ret                                                  // Return
}

这里都调用构造函数,甚至发布版本也这样做,因为它必须确保任何副作用仍然发生。

如果Customer 是引用类型,还会发生更多情况。如果它是一个值类型,那么所有它都保存在堆栈中(尽管它可能具有依次是引用类型的字段)。如果它是引用类型,那么堆栈中保存的是对堆中对象的引用。当堆栈上不再有任何此类引用时,垃圾收集器将不会在其扫描中找到它以找到它无法收集的对象,并且可以将其收集。

在发布版本中,一旦构造函数返回,可能永远不会有保存该引用的内存位置或寄存器。事实上,即使构造函数正在运行(如果没有字段访问或其他隐式或显式使用 this 发生)也可能没有,或者它可能已被擦除中途(一旦这样的访问已完成),因此垃圾收集可能在构造函数完成之前发生。

更有可能在方法返回后它会在堆内存中停留一段时间,因为 GC 还没有运行。

【讨论】:

    【解决方案2】:

    在第一种情况下,number 是一个值类型,将存储在堆栈中。一旦您离开该方法,它将不再存在。并且不会进行重新分配,堆栈空间只会用于其他事情。

    在第二种情况下,由于Customer(我假设)是一个引用类型,myClient 将在堆栈上存储对实例的引用。一旦您离开该方法,该引用就不再存在。这意味着实例最终会被垃圾回收。

    【讨论】:

    • 不太可能。 number 很可能会被简单地保存在一个寄存器中。无需为此进行昂贵的堆栈操作。如果它甚至首先被“分配”,那就是:D
    • 对象可以在你离开方法之前被垃圾回收。
    • 你们当然都是正确的。但是,为了让初学者保持简单,我想我的回答会做:)
    【解决方案3】:

    假设您在 Debug 下运行,没有进行任何优化:

    当它超出代码块(第 5 行)时,变量会发生什么 号码?

    一旦方法退出,值就会从堆栈中弹出。值类型存在于堆栈中这一事实是实现细节,您不应该依赖它。如果这是一个值类型,它是 class 中的一个字段,它就不会存在于堆栈中,而是存在于堆中。

    当它超出代码块(第 5 行)时,变量会发生什么 号码?

    假设Customerclass 而不是struct,并且没有实现终结器(这会改变事情的进程),那么在第 9 行之后它将不再有任何对象引用它。一旦 GC 启动(在任意的、非确定性的时间),它将认为它有资格收集并在标记阶段将其标记为如此。一旦扫描阶段开始,它将释放占用的内存。

    【讨论】:

      【解决方案4】:

      在 99% 的情况下,答案是“没关系”。唯一重要的是您无法再访问它。

      您不必太在意剩下的 1%。要对 SO 做出合理的回答,要充分简化这一点并不容易。我唯一能说的很简单的是:

      • 一旦将来不再使用变量,编译器或运行时就可以随心所欲地做任何事情是完全合法的 :)

      请注意,这并没有提到任何关于范围的内容 - C# 实际上并不关心范围。作用域可以帮助您编写代码,而不是帮助编译器(尽管方法和更高的作用域肯定有助于编译时间)。

      同样,在大多数情况下,您并不关心接下来会发生什么。主要的例外是:

      • 使用非托管资源。确定性地处置非托管资源通常是一个好主意。所有封装非托管资源的类都有一个Dispose 方法来处理这个问题。您可以使用using 语句来帮助解决此问题。
      • 性能瓶颈 - 如果分析显示您在不切实际的释放中丢失了内存/CPU,您可能需要提供一些帮助。
      • 保持对范围之外的对象的引用。很容易意外地阻止收集不再使用但仍有参考的东西。这是托管应用程序内存泄漏的主要原因。

      另外,如果你有时间玩这个,请注意,默认情况下,附加调试器会有点麻烦。例如,本地人将一直存活到其作用域结束 - 这当然是完全合法的,并且在调试时会有所帮助。

      【讨论】:

        【解决方案5】:

        当对结构类型字段的引用丢失时 - 内存被释放(在堆栈中)。对于引用类型,它更复杂。如果对象(类)不再使用并且对它的引用丢失,则垃圾收集器将其标记为删除。如果没有任何变化,则在下次垃圾回收时删除此对象。

        如果你不想等待 GC 自动运行它的方法,你可以通过调用 GC.Collect() 方法自己来完成

        附: 在你的类对象被销毁并释放内存之前(如果它实现了 IDisposable 接口),它会依次调用三个方法:

         1. Dispose() 2. Finalize() 3. ~ctor()
        

        在 C# 中,您可以使用其中的两个:dispose() 和 finalize()。当 Finalize 更适合编写非托管资源释放的逻辑时,Dispose 一般用于释放托管资源(例如FileStreamThreads)。

        要更改 object.Finalize() 方法的逻辑 - 将您的逻辑放入 ~ctor() 但要小心,因为它可能会导致一些严重的故障。

        【讨论】:

          猜你喜欢
          • 2017-01-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-10-03
          • 1970-01-01
          • 2019-04-03
          • 2011-07-12
          • 1970-01-01
          相关资源
          最近更新 更多