在 C# 中释放内存的正确方法是什么?

本文关键字:方法 是什么 释放 内存 | 更新日期: 2023-09-27 17:56:55

我在C#中有一个计时器,它可以在其方法中执行一些代码。在代码中,我使用了几个临时对象。

  1. 如果我在方法中有类似 Foo o = new Foo(); 的东西,这是否意味着每次计时器滴答作响时,我都会创建一个新对象和对该对象的新引用?

  2. 如果我有string foo = null,然后我只是在 foo 中放了一些时间的东西,它和上面一样吗?

  3. 垃圾回收器是否曾经删除过该对象,并且引用或对象会不断创建并保留在内存中?

  4. 如果我只是声明Foo o;而不将其指向任何实例,那么该方法结束时不是会释放吗?

  5. 如果我想确保删除所有内容,最好的方法是什么:

    • 方法内部使用 using 语句
    • 通过在最后调用释放方法
    • 通过将Foo o;放在计时器的方法之外,只需将赋值o = new Foo()在内部,那么在方法结束后删除指向对象的指针,垃圾回收器将删除该对象。

在 C# 中释放内存的正确方法是什么?

1.如果我在方法中有类似 Foo o = new Foo(); 的东西,这样做吗 意味着每次计时器滴答作响时, 我正在创建一个新对象和一个新的 引用该对象?

是的。

2.如果我有字符串 foo = null,然后我只是在 foo 中放入一些时间, 和上面一样吗?

如果您询问行为是否相同,那么是的。

3.垃圾回收器是否曾经删除过对象和引用或 对象不断创建并 留在记忆中?

这些对象使用的内存肯定是在引用被视为未使用之后收集的。

4.如果我只是声明Foo o;而不是指向任何实例,那不是吗? 方法结束时处理?

不,由于没有创建对象,因此没有要收集的对象(释放不是正确的词)。

5.如果我想确保删除所有内容,最好的方法是什么 做起来

如果对象的类实现了IDisposable那么你当然希望尽快贪婪地调用Disposeusing 关键字使此操作更容易,因为它以异常安全的方式自动调用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. 是的,每当执行此语句时,您都会创建一个新对象,但是,当您退出该方法时,它"超出范围"并且它符合垃圾回收的条件。
  2. 好吧,这将与#1相同,只是您使用了字符串类型。 字符串类型是不可变的,每次进行赋值时都会获得一个新对象。
  3. 是的,垃圾回收器会收集超出范围的对象,除非您将对象分配给具有较大范围的变量(如类变量)。
  4. 是的。
  5. using 语句仅适用于实现 IDisposable 接口的对象。如果是这种情况,请务必对方法范围内的对象使用 is 最佳。 不要把Fooo放在更大的范围内,除非你有充分的理由这样做。 最好将任何变量的作用域限制为有意义的最小作用域。

以下是快速概述:

  • 引用消失后,您的对象可能会被垃圾回收。
  • 您只能依靠统计收集来保持堆大小正常,前提是所有对垃圾的引用都真的消失了。换句话说,不能保证特定对象会被垃圾回收。
    • 因此,您的终结器也永远不会被调用。避免终结器。
  • 泄漏的两个常见来源:
    • 事件处理程序和委托是引用。如果您订阅对象的事件,则引用该事件。如果您有对象方法的委托,则引用它。
    • 根据定义,不会自动收集非托管资源。这就是 IDisposable 模式的用途。
  • 最后,如果你想要一个不阻止对象被收集的引用,请查看弱引用。

最后一件事:如果您在没有分配的情况下声明Foo foo;,则不必担心 - 不会泄露任何内容。如果 Foo 是引用类型,则不会创建任何内容。如果Foo是值类型,则会在堆栈上分配它,因此将自动清理。

  1. 是的
  2. 你说的一样是什么意思?每次运行该方法时,都会重新执行它。
  3. 是的,.Net 垃圾回收器使用的算法从任何全局/范围内变量开始,遍历它们,同时遵循它递归找到的任何引用,并删除内存中被视为无法访问的任何对象。有关垃圾回收的更多详细信息,请参阅此处
  4. 是的,
  5. 当方法退出时,方法中声明的所有变量的内存都会释放,因为它们都无法访问。此外,任何声明但从未使用的变量都将被编译器优化出来,因此实际上您的Foo变量永远不会占用内存。
  6. 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 将返回。