在 C# 中释放内存的正确方法是什么?
本文关键字:方法 是什么 释放 内存 | 更新日期: 2023-09-27 17:56:55
我在C#中有一个计时器,它可以在其方法中执行一些代码。在代码中,我使用了几个临时对象。
-
如果我在方法中有类似
Foo o = new Foo();
的东西,这是否意味着每次计时器滴答作响时,我都会创建一个新对象和对该对象的新引用? -
如果我有
string foo = null
,然后我只是在 foo 中放了一些时间的东西,它和上面一样吗? -
垃圾回收器是否曾经删除过该对象,并且引用或对象会不断创建并保留在内存中?
-
如果我只是声明
Foo o;
而不将其指向任何实例,那么该方法结束时不是会释放吗? -
如果我想确保删除所有内容,最好的方法是什么:
- 方法内部使用 using 语句
- 通过在最后调用释放方法
- 通过将
Foo o;
放在计时器的方法之外,只需将赋值o = new Foo()
在内部,那么在方法结束后删除指向对象的指针,垃圾回收器将删除该对象。
1.如果我在方法中有类似 Foo o = new Foo(); 的东西,这样做吗 意味着每次计时器滴答作响时, 我正在创建一个新对象和一个新的 引用该对象?
是的。
2.如果我有字符串 foo = null,然后我只是在 foo 中放入一些时间, 和上面一样吗?
如果您询问行为是否相同,那么是的。
3.垃圾回收器是否曾经删除过对象和引用或 对象不断创建并 留在记忆中?
这些对象使用的内存肯定是在引用被视为未使用之后收集的。
4.如果我只是声明Foo o;而不是指向任何实例,那不是吗? 方法结束时处理?
不,由于没有创建对象,因此没有要收集的对象(释放不是正确的词)。
5.如果我想确保删除所有内容,最好的方法是什么 做起来
如果对象的类实现了IDisposable
那么你当然希望尽快贪婪地调用Dispose
。using
关键字使此操作更容易,因为它以异常安全的方式自动调用Dispose
。
除此之外,除了停止使用该对象之外,您实际上不需要执行任何其他操作。如果引用是局部变量,则当它超出范围时,它将有资格进行收集。1 如果它是一个类级别变量,则可能需要为其分配null
以使其符合条件,然后才能使包含的类符合条件。
1这在技术上是不正确的(或者至少有点误导)。对象在超出范围之前很久就有资格进行收集。CLR 经过优化,可在检测到不再使用引用时收集内存。在极端情况下,CLR 可以收集对象,即使其方法之一仍在执行!
更新:
以下示例演示了 GC 将收集对象,即使它们可能仍在范围内。您必须编译发布版本并在调试器外部运行它。
static void Main(string[] args)
{
Console.WriteLine("Before allocation");
var bo = new BigObject();
Console.WriteLine("After allocation");
bo.SomeMethod();
Console.ReadLine();
// The object is technically in-scope here which means it must still be rooted.
}
private class BigObject
{
private byte[] LotsOfMemory = new byte[Int32.MaxValue / 4];
public BigObject()
{
Console.WriteLine("BigObject()");
}
~BigObject()
{
Console.WriteLine("~BigObject()");
}
public void SomeMethod()
{
Console.WriteLine("Begin SomeMethod");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("End SomeMethod");
}
}
在我的机器上,终结器在SomeMethod
仍在执行时运行!
.NET 垃圾回收器会为您处理所有这些工作。
它能够确定何时不再引用对象,并将(最终)释放分配给它们的内存。
范围变得无法访问,就有资格进行垃圾回收(谢谢 ben!除非垃圾回收器认为内存不足,否则不会释放内存。
对于托管资源,垃圾回收器将知道何时是,您无需执行任何操作。
对于非托管资源(例如与数据库的连接或打开的文件),垃圾回收器无法知道它们消耗了多少内存,这就是为什么您需要手动释放它们(使用 dispose,或者更好的是使用 using 块)
如果未释放对象,则要么您还剩下大量内存并且不需要,要么您在应用程序中维护对它们的引用,因此垃圾回收器不会释放它们(如果您实际使用您维护的此引用)
让我们一一回答您的问题。
- 是的,每当执行此语句时,您都会创建一个新对象,但是,当您退出该方法时,它"超出范围"并且它符合垃圾回收的条件。
- 好吧,这将与#1相同,只是您使用了字符串类型。 字符串类型是不可变的,每次进行赋值时都会获得一个新对象。
- 是的,垃圾回收器会收集超出范围的对象,除非您将对象分配给具有较大范围的变量(如类变量)。
- 是的。
- using 语句仅适用于实现 IDisposable 接口的对象。如果是这种情况,请务必对方法范围内的对象使用 is 最佳。 不要把Fooo放在更大的范围内,除非你有充分的理由这样做。 最好将任何变量的作用域限制为有意义的最小作用域。
以下是快速概述:
- 引用消失后,您的对象可能会被垃圾回收。
- 您只能依靠统计收集来保持堆大小正常,前提是所有对垃圾的引用都真的消失了。换句话说,不能保证特定对象会被垃圾回收。
- 因此,您的终结器也永远不会被调用。避免终结器。
- 泄漏的两个常见来源:
- 事件处理程序和委托是引用。如果您订阅对象的事件,则引用该事件。如果您有对象方法的委托,则引用它。
- 根据定义,不会自动收集非托管资源。这就是 IDisposable 模式的用途。
- 最后,如果你想要一个不阻止对象被收集的引用,请查看弱引用。
最后一件事:如果您在没有分配的情况下声明Foo foo;
,则不必担心 - 不会泄露任何内容。如果 Foo 是引用类型,则不会创建任何内容。如果Foo是值类型,则会在堆栈上分配它,因此将自动清理。
- 是的
- 你说的一样是什么意思?每次运行该方法时,都会重新执行它。
- 是的,.Net 垃圾回收器使用的算法从任何全局/范围内变量开始,遍历它们,同时遵循它递归找到的任何引用,并删除内存中被视为无法访问的任何对象。有关垃圾回收的更多详细信息,请参阅此处 是的,
- 当方法退出时,方法中声明的所有变量的内存都会释放,因为它们都无法访问。此外,任何声明但从未使用的变量都将被编译器优化出来,因此实际上您的
Foo
变量永远不会占用内存。 using
语句只是在IDisposable
对象退出时对它调用 Dispose 处理,因此这等效于您的第二个项目符号点。两者都将指示您已完成该对象,并告诉 GC 您已准备好放开它。覆盖对对象的唯一引用将具有类似的效果。
垃圾回收器将出现并清理不再引用它的任何内容。除非你Foo
里面有非托管的资源,否则调用Dispose或在其上使用使用语句不会真正帮助你。
我相当确定这适用,因为它仍然在 C# 中。但是,我参加了使用XNA的游戏设计课程,我们花了一些时间讨论C#的垃圾回收器。垃圾回收是昂贵的,因为您必须检查是否有对要收集的对象的任何引用。因此,GC试图尽可能长时间地推迟这一点。因此,只要您的程序达到 700MB 时物理内存没有耗尽,这可能只是 GC 懒惰并且还不担心它。
但是,如果您只是在循环外使用Foo o
并且每次都创建一个o = new Foo()
,那么一切都应该很好。
正如 Brian 指出的那样,GC 可以收集任何无法访问的内容,包括仍在范围内的对象,甚至这些对象的实例方法仍在执行时也是如此。 请考虑以下代码:
class foo
{
static int liveFooInstances;
public foo()
{
Interlocked.Increment(ref foo.liveFooInstances);
}
public void TestMethod()
{
Console.WriteLine("entering method");
while (Interlocked.CompareExchange(ref foo.liveFooInstances, 1, 1) == 1)
{
Console.WriteLine("running GC.Collect");
GC.Collect();
GC.WaitForPendingFinalizers();
}
Console.WriteLine("exiting method");
}
~foo()
{
Console.WriteLine("in ~foo");
Interlocked.Decrement(ref foo.liveFooInstances);
}
}
class Program
{
static void Main(string[] args)
{
foo aFoo = new foo();
aFoo.TestMethod();
//Console.WriteLine(aFoo.ToString()); // if this line is uncommented TestMethod will never return
}
}
如果使用调试版本运行,则附加调试器或指定行未注释 TestMethod 将永远不会返回。但是在没有附加调试器的情况下运行 TestMethod 将返回。