在 C# 中释放内存的正确方法是啥

Posted

技术标签:

【中文标题】在 C# 中释放内存的正确方法是啥【英文标题】:What is the correct way to free memory in C#在 C# 中释放内存的正确方法是什么 【发布时间】:2011-08-29 07:13:48 【问题描述】:

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

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

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

    垃圾收集器是否会删除对象,并且引用或对象会不断创建并保留在内存中?

    如果我只是声明Foo o;而不将它指向任何实例,那不是在方法结束时释放了吗?

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

    在方法中使用 using 语句 最后调用 dispose 方法 通过把Foo o;放在定时器的方法外面,把o = new Foo()赋值在里面,这样在方法结束后,指向对象的指针就被删除了,垃圾回收器就会删除这个对象。

【问题讨论】:

【参考方案1】:

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

是的。

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

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

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

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

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

不,因为没有创建对象,所以没有要收集的对象(dispose 不是正确的词)。

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 仍在执行时运行!

【讨论】:

如果该对象没有根引用,您将如何最终进入该对象的方法? @Yaur:好问题:考虑一个不使用其他实例成员(变量或方法)的实例方法。这意味着只需要提取对象引用即可传递this 引用。之后,只要 CLR 可以检测到它以后没有被使用,即使它可能仍然是根,该对象在技术上是合格的。 @Brian Gideon 我明白你的意思,但我持怀疑态度,因为 CLR 使用标记和清除来确定 GC 资格是否会发生,或者该对象在技术上甚至是垃圾的“资格”收藏。但无论如何,这是一个足够有趣的案例,可以进行一些测试。 @Yaur:很容易证明可以在对象仍处于根状态时对其进行收集。通过简单的构造函数调用在Main 方法中创建一个非常大的对象并将其分配给本地引用。然后开始分配更多内存来触发 GC,但请确保不要在 Main 方法中再次使用该引用。您可能需要在对象上定义析构函数,以便您可以验证终结器是否在在引用超出范围之前调用。确保在调试器之外使用 Release 版本运行它。 @Yaur:我发布了一个示例来演示我在说什么。请注意,我怀疑这种行为可能高度依赖于正在测试的 CLR 版本。早期版本可能没有此优化。这是一个相当有趣的演示,展示了 GC 的攻击性。【参考方案2】:

.NET 垃圾收集器会为您处理所有这些。

它能够确定何时不再引用对象并(最终)释放已分配给它们的内存。

【讨论】:

你确定连定时器内部创建的对象都被删除了吗?因为我遇到了一个问题,我的应用程序在 10 分钟内是 700mb,我猜测应该在计时器中删除的对象出现问题。 除非您正在做一些事情来保留在计时器的“tick”方法范围内创建的那些引用,否则它们将在方法退出后在某个时间被释放.这取决于垃圾收集器何时运行并检测到没有任何东西仍然指向这些对象。如果您的计时器在足够快的周期内滴答作响,我可以看到如何消耗大量内存。 它会处理所有这些......除非它没有。不用担心内存管理是让堆增长失控的好方法。【参考方案3】:

一旦对象超出范围变得无法访问(感谢 ben!),它们就有资格进行垃圾收集。除非垃圾收集器认为您的内存不足,否则不会释放内存。

对于托管资源,垃圾收集器会知道这是什么时候,你不需要做任何事情。

对于非托管资源(例如与数据库的连接或打开的文件),垃圾收集器无法知道它们消耗了多少内存,这就是您需要手动释放它们的原因(使用 dispose,或者更好的是使用块)

如果对象没有被释放,要么你有足够的内存并且没有必要,要么你在你的应用程序中维护对它们的引用,因此垃圾收集器不会释放它们(如果你实际使用你维护的这个参考)

【讨论】:

s/超出范围/无法访问/。范围实际上只与值类型的(未捕获的)局部变量相关。 关于您提到的打开的文件,磁盘上的文件大小与文件在内存中的内存量相同(假设它没有被改变)。 除非您将整个文件读入内存,否则不会。当您打开文件时,操作系统可以以不同的方式(读/写等)锁定它。垃圾收集是不确定的(即它在需要时发生),因此如果文件句柄没有显式释放,文件可能会保持锁定更长时间。文件句柄也是有限的,垃圾收集器不知道这些,这就是为什么文件应该关闭,而不是留给垃圾收集器【参考方案4】:

这里是一个快速概述:

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

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

【讨论】:

【参考方案5】:
    是的 你说的相同是什么意思?每次运行该方法时都会重新执行。 是的,.Net 垃圾收集器使用一种算法,该算法从任何全局/范围内变量开始,遍历它们,同时跟踪它找到的任何引用递归,并删除内存中被认为无法访问的任何对象。 see here for more detail on Garbage Collection 是的,当方法退出时,方法中声明的所有变量的内存都会被释放,因为它们都无法访问。此外,任何已声明但从未使用过的变量都会被编译器优化掉,因此实际上您的 Foo 变量永远不会占用内存。 using 语句在退出时仅在 IDisposable 对象上调用 dispose,因此这相当于您的第二个要点。两者都将表明您已完成该对象并告诉 GC 您已准备好放开它。覆盖对对象的唯一引用将产生类似的效果。

【讨论】:

CrazyJugglerDrummer 提供的链接非常适合阅读 GC。【参考方案6】:

让我们一一回答你的问题。

    是的,每当执行此语句时,您都会创建一个新对象,但是,当您退出该方法时,它会“超出范围”并且符合垃圾回收条件。 这与#1 相同,只是您使用了字符串类型。字符串类型是不可变的,每次进行赋值都会得到一个新对象。 是的,垃圾收集器会收集超出范围的对象,除非您将对象分配给具有较大范围的变量,例如类变量。 是的。 using 语句仅适用于实现 IDisposable 接口的对象。如果是这种情况,那么无论如何 using 最适合方法范围内的对象。除非您有充分的理由这样做,否则不要将 Foo o 放在更大的范围内。最好将任何变量的范围限制在有意义的最小范围内。

【讨论】:

【参考方案7】:

垃圾收集器会出现并清理任何不再引用它的东西。除非您在 Foo 中有非托管资源,否则调用 Dispose 或在其上使用 using 语句不会对您有太大帮助。

我很确定这适用,因为它仍在 C# 中。但是,我参加了一个使用 XNA 的游戏设计课程,我们花了一些时间讨论 C# 的垃圾收集器。垃圾收集很昂贵,因为您必须检查是否有任何对要收集的对象的引用。所以,GC 会尽量推迟这个时间。所以,只要你的程序达到 700MB 时没有耗尽物理内存,那可能只是 GC 比较懒惰,还没有担心。

但是,如果您只是在循环外使用 Foo o 并每次都创建一个 o = new Foo(),那么应该一切正常。

【讨论】:

【参考方案8】:

正如 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 会返回。

【讨论】:

以上是关于在 C# 中释放内存的正确方法是啥的主要内容,如果未能解决你的问题,请参考以下文章

Three.js Collada - dispose() 和释放内存(垃圾收集)的正确方法是啥?

移除对象时内存未释放 - 不清楚在 ARC 中释放的正确方法

在c ++中删除和释放向量内存的正确方法(防止不同的内存相关错误)

托管和非托管的c++是啥意思,有啥区别?

c#爬虫-从内存中释放Selenium chromedriver.exe终极杀

释放返回变量内存的正确方法