变量生命周期

5
当执行行超出代码块时,变量会发生什么情况?例如:
1  public void myMethod()
2  {
3     int number;
4     number = 5;
5  }

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

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

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

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

我猜在两种情况下变量都被分配了,但是何时被释放呢?


2
https://dev59.com/_m035IYBdhLWcg3wbPfc - Tim Schmelter
我敢打赌编译器甚至不会在第一个方法内执行任何操作。第二个方法将不得不调用构造函数,但实例不会被存储在任何地方,因为它没有被使用。 - Sinatr
对于初学者程序员(我就是其中之一),垃圾回收并不是初学者应该关注的话题。因此,我的问题是合理的,因为初学者不会遇到垃圾回收这个主题。我已经以一种适合初学者理解的方式构建了问题,因为我们经常听到“它不在范围内”,这并不能完全澄清正在发生的事情。 - ExpertLoser
主要问题是你不应该真的关心发生了什么 - 这是语言的“托管”部分。只有当你开始进行性能调整时,你才真正需要关心,而这确实不是初学者的主题。但请注意,如果你的C#书籍/教程/示例告诉你变量离开作用域时内存被释放,那么它是错误的(或者至少是误导性的)。作用域的主要目的是为了简化编程 - 你不能再访问局部变量,所以你不必在作用域之外一直考虑它。 - Luaan
2
我认为这不完全是一个重复的问题,它询问了除了GC部分之外的位(尽管那也是相关的)。 - Jon Hanna
5个回答

8
作为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
}

调试版的保存内容时间更长,因为检查它们对于调试很有用。

当发布版本在本地变量数组中存储东西时,它们也更有可能重复使用方法内部的插槽。

然后将其转换为机器代码。这类似于它的工作方式,它会产生数字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发生),甚至可能没有一个,或者在其中间部分被擦除(一旦这些访问完成),因此在构造函数甚至还没有完成之前就可以进行垃圾回收。
更有可能的是,在方法返回后,它会在堆内存中挂起一段时间,因为垃圾回收尚未运行。

1
当结构体类型的字段引用丢失时,内存会被释放(在堆栈中)。对于引用类型来说,情况更加复杂。如果对象(类)不再使用并且对其的引用丢失,则垃圾回收器将标记其为待删除状态。如果没有任何更改,该对象将在下一次垃圾回收时被删除。
如果您不想等待GC自动运行其方法,可以通过调用GC.Collect()方法自己运行它。
附注: 在销毁类对象并释放内存之前(如果它实现了IDisposable接口),它按顺序调用三个方法:
 1. Dispose() 2. Finalize() 3. ~ctor()

在C#中,您可以使用两种方法:dispose()和finalize()。Dispose通常用于释放托管资源(例如FileStream或Threads),而Finalize更适合编写未托管资源释放的逻辑。
要更改object.Finalize()方法的逻辑 - 将您的逻辑放入~ctor()中 但是要小心,因为这可能会导致一些严重的故障。

1
在99%的情况下,答案是“无关紧要”。唯一重要的是它对你不再可用。
你不应该过于关注剩下的1%。简化这个问题并提供一个合理的SO答案并不容易。我能够提供的唯一简单的建议是:
只要变量在未来不再被使用,编译器或运行时可以随便做任何事情,完全合法 :)
请注意,这并没有提到范围的任何内容-C#实际上并不太关心范围。范围存在是为了帮助您编写代码,而不是为了帮助编译器(尽管方法和更高级别的作用域确实有助于编译时间)。
同样,在大多数情况下,您不关心接下来会发生什么。主要的例外情况是:
  • 使用非托管资源。通常最好确定性地处理非托管资源。封装非托管资源的所有类都有一个Dispose方法来处理这个问题。您可以使用using语句来帮助解决。
  • 性能瓶颈-如果分析表明您在不切实际的释放上正在失去内存/CPU,您可能需要进行一些帮助。
  • 保留对象的引用外部范围。很容易意外地阻止收集某些不再使用但仍具有引用的东西。这是托管应用程序中内存泄漏的主要原因。

此外,如果您想玩一下这个,注意默认情况下,连接了调试器会对您造成一些干扰。例如,本地变量将保持活动状态,直到其作用域结束-当然,在调试时这是完全合法的,并且有助于调试。


1
在第一种情况下,number 是一个值类型,将存储在堆栈上。一旦离开方法,它将不再存在。没有任何释放操作,堆栈空间将被用于其他事情。
在第二种情况下,由于 Customer(我想)是一个引用类型,myClient 将存储对实例的引用在堆栈上。一旦离开方法,该引用不再存在。这意味着实例最终将被垃圾回收。

不太可能。number 很有可能只是被存储在寄存器中,没有必要为此进行昂贵的堆栈操作。如果它真的被“分配”了,那就更好了 :D - Luaan
对象可以在您离开方法之前被垃圾回收。 - Jon Hanna
你们当然都是正确的。但是,为了让初学者更容易理解,我认为我的答案就可以了 :) - Chris

1

假设您在没有优化的情况下运行调试:

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

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

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

假设Customer是一个class而不是struct,并且没有实现finalizer(这会改变事情的进程),在第9行后,它将不再有任何对象引用它。一旦GC启动(在任意的、不确定的时间),它会在标记阶段将其视为可收集的,并将其标记为可回收垃圾。一旦扫描阶段开始,它就会释放占用的内存。


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