44

.NET 垃圾收集器最终会释放内存,但如果您想立即恢复内存怎么办?你需要在类中使用什么代码MyClass来调用

MyClass.Dispose()

并通过变量和对象释放所有使用的空间MyClass

4

20 回答 20

101

IDisposable 与释放内存无关。IDisposable 是一种释放非托管资源的模式——内存绝对是一种托管资源。

指向 GC.Collect() 的链接是正确答案,尽管 Microsoft .NET 文档通常不鼓励使用此函数。

编辑:已经为这个答案赢得了大量的业力,我觉得有一定的责任来详细说明它,以免.NET 资源管理的新手产生错误的印象。

在 .NET 进程中,有两种资源——托管和非托管。“托管”意味着运行时控​​制资源,而“非托管”意味着它是程序员的责任。今天,我们在.NET 中真正关心的托管资源只有一种——内存。程序员告诉运行时分配内存,然后由运行时确定何时可以释放内存。.NET 用于此目的的机制称为垃圾收集,您只需使用 Google 就可以在 Internet 上找到大量有关 GC 的信息。

对于其他类型的资源,.NET 对清理它们一无所知,因此它必须依靠程序员来做正确的事情。为此,平台为程序员提供了三种工具:

  1. VB 和 C# 中的 IDisposable 接口和“使用”语句
  2. 终结者
  3. 许多 BCL 类实现的 IDisposable 模式

其中第一个允许程序员有效地获取资源,使用它,然后在同一个方法中释放它。

using (DisposableObject tmp = DisposableObject.AcquireResource()) {
    // Do something with tmp
}
// At this point, tmp.Dispose() will automatically have been called
// BUT, tmp may still a perfectly valid object that still takes up memory

如果“AcquireResource”是(例如)打开文件并且“Dispose”自动关闭文件的工厂方法,则此代码不能泄漏文件资源。但是“tmp”对象本身的内存可能仍然被分配。那是因为 IDisposable 接口与垃圾收集器完全没有联系。如果您确实想确保内存被释放,您唯一的选择是调用GC.Collect()强制垃圾回收。

但是,无论如何强调这可能不是一个好主意。通常让垃圾收集器完成它的设计目的要好得多,即管理内存。

如果资源使用时间较长,以至于它的生命周期跨越了几种方法,会发生什么?显然,“using”语句不再适用,因此程序员在使用完资源后必须手动调用“Dispose”。如果程序员忘记了会发生什么?如果没有回退,那么进程或计算机最终可能会用尽没有正确释放的任何资源。

这就是终结器的用武之地。终结器是您的类上与垃圾收集器有特殊关系的方法。GC 承诺——在为该类型的任何对象释放内存之前——它将首先让终结器有机会进行某种清理。

所以在文件的情况下,理论上我们根本不需要手动关闭文件。我们可以等到垃圾收集器到达它,然后让终结器完成工作。不幸的是,这在实践中效果不佳,因为垃圾收集器运行不确定。该文件可能保持打开的时间比程序员预期的要长得多。如果有足够多的文件保持打开状态,则系统在尝试打开其他文件时可能会失败。

对于大多数资源,我们想要这两样东西。我们希望约定能够说“我们现在已经完成了这个资源”,并且我们希望确保如果我们忘记手动进行清理,至少有一些机会自动进行清理。这就是“IDisposable”模式发挥作用的地方。这是一个允许 IDispose 和终结器很好地一起运行的约定。您可以通过查看IDisposable 的官方文档来了解该模式的工作原理。

底线:如果您真正想做的只是确保释放内存,那么 IDisposable 和终结器将无济于事。但是 IDisposable 接口是所有 .NET 程序员都应该理解的极其重要的模式的一部分。

于 2008-08-15T15:40:33.567 回答
23

您只能释放实现 IDisposable 接口的实例。

要强制垃圾收集立即释放(非托管)内存:

GC.Collect();  
GC.WaitForPendingFinalizers();

这通常是不好的做法,但例如在 x64 版本的 .NET 框架中存在一个错误,它使 GC 在某些情况下表现得奇怪,然后您可能想要这样做。不知道bug是否已经解决了。有人知道吗?

要处置一个类,您可以这样做:

instance.Dispose();

或像这样:

using(MyClass instance = new MyClass())
{
    // Your cool code.
}

这将在编译时转换为:

MyClass instance = null;    

try
{
    instance = new MyClass();        
    // Your cool code.
}
finally
{
    if(instance != null)
        instance.Dispose();
}

您可以像这样实现 IDisposable 接口:

public class MyClass : IDisposable
{
    private bool disposed;

    /// <summary>
    /// Construction
    /// </summary>
    public MyClass()
    {
    }

    /// <summary>
    /// Destructor
    /// </summary>
    ~MyClass()
    {
        this.Dispose(false);
    }

    /// <summary>
    /// The dispose method that implements IDisposable.
    /// </summary>
    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// The virtual dispose method that allows
    /// classes inherithed from this one to dispose their resources.
    /// </summary>
    /// <param name="disposing"></param>
    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources here.
            }

            // Dispose unmanaged resources here.
        }

        disposed = true;
    }
}
于 2008-08-15T15:48:42.957 回答
19

对这个问题的回答有点混乱。

标题询问了处置,但随后说他们希望立即恢复记忆。

.Net 是托管的,这意味着当您编写 .Net 应用程序时,您无需直接担心内存,代价是您也无法直接控制内存。

.Net 决定何时最好清理和释放内存,而不是您作为 .Net 编码器。

Dispose是一种告诉 .Net 你已经完成了某些事情的方法,但它实际上不会释放内存,直到它是这样做的最佳时间。

基本上.Net实际上会在最容易的时候收集内存——它非常擅长决定什么时候。除非你正在写一些非常占用内存的东西,否则你通常不需要推翻它(这是游戏不经常用 .Net 编写的部分原因——它们需要完全控制)

在 .Net 中,您可以使用GC.Collect()它来立即强制它,但这几乎总是不好的做法。如果.Net 还没有清理它,这意味着它不是清理它的好时机。

GC.Collect()拾取 .Net 识别为已完成的对象。如果您尚未处理需要它的对象,.Net 可能会决定保留该对象。这意味着GC.Collect()只有在您正确实施一次性实例时才有效。

GC.Collect()不能替代正确使用 IDisposable。

所以 Dispose 和 memory 没有直接关系,但也没有必要。正确处理将使您的 .Net 应用程序更高效,因此使用更少的内存。


在 .Net 中 99% 的情况下,以下是最佳实践:

规则 1:如果你不处理任何非托管或实现的东西,IDisposable 那么不要担心 Dispose。

规则 2:如果您有一个实现 IDisposable 的局部变量,请确保在当前范围内摆脱它:

//using is best practice
using( SqlConnection con = new SqlConnection("my con str" ) )
{
    //do stuff
} 

//this is what 'using' actually compiles to:
SqlConnection con = new SqlConnection("my con str" ) ;
try
{
    //do stuff
}
finally
{
    con.Dispose();
}

规则 3:如果一个类具有实现 IDisposable 的属性或成员变量,那么该类也应该实现 IDisposable。在该类的 Dispose 方法中,您还可以处理您的 IDisposable 属性:

//rather basic example
public sealed MyClass :
   IDisposable
{   
    //this connection is disposable
    public SqlConnection MyConnection { get; set; }

    //make sure this gets rid of it too
    public Dispose() 
    {
        //if we still have a connection dispose it
        if( MyConnection != null )
            MyConnection.Dispose();

        //note that the connection might have already been disposed
        //always write disposals so that they can be called again
    }
}

这并不完整,这就是示例被密封的原因。继承类可能需要遵守下一条规则......

规则 4:如果类使用非托管资源,则实现 IDispose添加终结器。

.Net 无法对非托管资源做任何事情,所以现在我们谈论的是内存。如果你不清理它,你可能会出现内存泄漏。

Dispose 方法需要同时处理托管非托管资源。

终结器是一个安全问题 - 它确保如果其他人创建了您的类的实例并且未能处理它,那么.Net 仍然可以清理“危险的”非托管资源。

~MyClass()
{
    //calls a protected method 
    //the false tells this method
    //not to bother with managed
    //resources
    this.Dispose(false);
}

public void Dispose()
{
    //calls the same method
    //passed true to tell it to
    //clean up managed and unmanaged 
    this.Dispose(true);

    //as dispose has been correctly
    //called we don't need the 

    //'backup' finaliser
    GC.SuppressFinalize(this);
}

最后这个 Dispose 的重载带有一个布尔标志:

protected virtual void Dispose(bool disposing)
{
    //check this hasn't been called already
    //remember that Dispose can be called again
    if (!disposed)
    {
        //this is passed true in the regular Dispose
        if (disposing)
        {
            // Dispose managed resources here.
        }

        //both regular Dispose and the finaliser
        //will hit this code
        // Dispose unmanaged resources here.
    }

    disposed = true;
}

请注意,一旦这一切就绪,其他创建类实例的托管代码就可以像对待任何其他 IDisposable 一样对待它(规则 2 和 3)。

于 2008-08-31T09:56:24.013 回答
14

还提到 dispose 并不总是指内存是否合适?我比内存更频繁地处理资源,例如对文件的引用。GC.Collect() 直接与 CLR 垃圾收集器相关,可能会也可能不会释放内存(在任务管理器中)。它可能会对您的应用程序产生负面影响(例如性能)。

归根结底,您为什么要立即恢复记忆?如果有来自其他地方的内存压力,操作系统会在大多数情况下为您提供内存。

于 2008-08-15T15:41:02.803 回答
6

看看这篇文章

实现 Dispose 模式、IDisposable 和/或终结器与内存何时回收完全无关;相反,它与告诉 GC如何回收该内存有关。当您调用 Dispose() 时,您绝不会与 GC 交互。

GC 只会在它确定需要(称为内存压力)时运行,然后(并且只有在那时)才会为未使用的对象释放内存并压缩内存空间。

可以调用 GC.Collect() 但你真的不应该调用,除非有很好的理由(几乎总是“从不”)。当您像这样强制执行带外收集周期时,实际上会导致 GC 做更多的工作,最终可能会损害您的应用程序性能。在 GC 收集周期期间,您的应用程序实际上处于冻结状态……运行的 GC 周期越多,您的应用程序冻结的时间就越多。

您还可以进行一些本机 Win32 API 调用来释放您的工作集,但即使是那些也应该避免,除非有很好的理由这样做。

垃圾收集运行时背后的整个前提是您不需要(尽可能多地)担心运行时何时分配/取消分配实际内存;您只需要担心确保您的对象在被询问时知道如何自行清理。

于 2008-08-17T14:22:44.940 回答
5

我在http://codingcraftsman.wordpress.com/2012/04/25/to-dispose-or-not-to-dispose/上写了 Destructors 和 Dispose 和 Garbage 收集的摘要

要回答原始问题:

  1. 不要试图管理你的记忆
  2. Dispose 不是关于内存管理,而是关于非托管资源管理
  3. 终结器是 Dispose 模式的固有部分,实际上会减慢托管对象的内存释放速度(因为它们必须进入终结队列,除非已经 Dispose d)
  4. GC.Collect 不好,因为它使一些短期对象似乎需要更长的时间,从而减慢它们被收集的速度。

但是,如果您有代码的性能关键部分并希望减少垃圾收集减慢它的可能性,GC.Collect 可能会很有用。你以前这么叫。

最重要的是,有一个支持这种模式的论点:

var myBigObject = new MyBigObject(1);
// something happens
myBigObject = new MyBigObject(2);
// at the above line, there are temporarily two big objects in memory and neither can be collected

对比

myBigObject = null; // so it could now be collected
myBigObject = new MyBigObject(2);

但主要的答案是垃圾收集只是工作,除非你乱用它!

于 2012-05-02T22:13:37.387 回答
3
public class MyClass : IDisposable
{
    public void Dispose()
    {
       // cleanup here
    }
}

那么你可以做这样的事情

MyClass todispose = new MyClass();
todispose.Dispose(); // instance is disposed right here

或者

using (MyClass instance = new MyClass())
{

}
// instance will be disposed right here as it goes out of scope
于 2008-08-15T15:32:02.413 回答
3

Joe Duffy 对“ Dispose, Finalization, and Resource Management ”的完整解释:

在 .NET Framework 生命周期的早期,C# 程序员一直将终结器称为析构函数。随着时间的推移,我们变得越来越聪明,我们试图接受这样一个事实,即Dispose 方法实际上更等同于 C++ 析构函数(确定性),而 终结器是完全独立的(非确定性)。C# 借用了 C++ 析构函数语法(即 ~T())这一事实肯定至少与这个误称的发展有一点关系。

于 2008-08-26T09:36:18.907 回答
2

您不能真正强制 GC 在需要时清理对象,尽管有一些方法可以强制它运行,但没有说它会清理您想要/期望的所有对象。最好以 try catch ex finally dispose end try (VB.NET rulz) 方式调用 dispose。但是 Dispose 用于清理对象以确定性方式分配的系统资源(内存、句柄、数据库连接等)。Dispose 不会(也不能)清理对象本身使用的内存,只有 GC可以做到这一点。

于 2008-08-15T15:42:36.217 回答
1

这篇文章有一个非常简单的演练。但是,不得不调用 GC 而不是让它自然运行通常是设计/内存管理不佳的标志,尤其是在没有消耗有限资源(连接、句柄、通常导致实现 IDisposable 的任何其他内容)的情况下。

是什么导致你需要这样做?

于 2008-08-15T15:30:57.393 回答
1

抱歉,这里选择的答案不正确。正如一些人随后所说的那样,Dispose 和实现 IDisposable 与释放与 .NET 类关联的内存无关。它主要和传统上用于释放非托管资源,例如文件句柄等。

虽然您的应用程序可以调用 GC.Collect() 来尝试强制垃圾收集器进行收集,但这只会对那些处于可访问队列中正确生成级别的项目产生影响。因此,如果您已经清除了对该对象的所有引用,那么在实际内存被释放之前,可能仍然需要对 GC.Collect() 进行几次调用。

你没有在你的问题中说为什么你觉得需要立即释放内存。我知道有时可能会出现不寻常的情况,但严重的是,在托管代码中,让运行时处理内存管理几乎总是最好的。

如果您认为您的代码比 GC 释放内存的速度更快地耗尽内存,这可能是最好的建议,那么您应该检查您的代码,以确保在静态成员等的任何数据结构中都没有引用不再需要的对象. 也尽量避免你有循环对象引用的情况,因为它们也可能不会被释放。

于 2008-08-25T21:29:10.013 回答
1

@基思,

我同意你的所有规则,除了#4。添加终结器只能在非常特定的情况下完成。如果一个类使用非托管资源,则应在 Dispose(bool) 函数中清理这些资源。当 bool 为真时,这个相同的函数应该只清理托管资源。添加终结器会增加使用对象的复杂性成本,因为每次创建新实例时,它还必须放在终结队列中,每次 GC 运行收集周期时都会检查该队列。实际上,这意味着您的对象在一个周期/代中的存活时间比它应该运行的时间长,因此可以运行终结器。终结器不应被视为“安全网”。

GC 只会在确定 Gen0 堆中没有足够的可用内存来执行下一次分配时运行一个收集周期,除非您通过调用 GC.Collect() 来“帮助”它强制进行带外收集.

底线是,无论如何,GC 只知道如何通过调用 Dispose 方法来释放资源(如果实现的话,可能还有终结器)。由该方法“做正确的事”并清理使用的任何非托管资源并指示任何其他托管资源调用其 Dispose 方法。只要没有带外收集周期的帮助,它的工作效率就很高,并且可以在很大程度上进行自我优化。话虽如此,如果没有显式调用 GC.Collect,您将无法控制何时以及以何种顺序处理对象和释放内存。

于 2008-08-31T10:26:30.247 回答
0

如果 MyClass 实现了 IDisposable 你就可以做到这一点。

MyClass.Dispose();

C# 的最佳实践是:

using( MyClass x = new MyClass() ) {
    //do stuff
}

因为这在 try-finally 中结束了处置,并确保它永远不会错过。

于 2008-08-15T15:32:40.337 回答
0

如果你不想(或不能)在你的类上实现 IDisposable,你可以像这样强制垃圾收集(但它很慢) -

GC.Collect();
于 2008-08-15T15:33:55.373 回答
0

IDisposable 接口实际上适用于包含非托管资源的类。如果您的类不包含非托管资源,为什么需要在垃圾收集器执行之前释放资源?否则,只需确保您的对象尽可能晚地实例化并尽快超出范围。

于 2008-08-15T16:48:49.967 回答
0

您可以在 C++ 中进行确定性对象破坏

你永远不想调用 GC.Collect,它会干扰垃圾收集器的自我调整以检测内存压力,并且在某些情况下除了增加堆上每个对象的当前代数之外什么也不做。

对于那些发布 IDisposable 答案的人。正如提问者所描述的那样,调用 Dispose 方法不会破坏对象。

于 2008-08-21T08:05:08.073 回答
0

@Curt Hagenlocher - 回到前面。我不知道为什么有这么多人在它错了的时候投了赞成票。

IDisposable用于托管资源。

终结者适用于非托管资源。

只要您只使用托管资源,@Jon Limjap 和我自己都是完全正确的。

对于使用非托管资源的类(请记住,绝大多数 .Net 类不使用)Patrik 的答案是全面的最佳实践。

避免使用 GC.Collect - 这是一种处理托管资源的缓慢方法,并且除非您正确构建了 ~Finalizers,否则不会对非托管资源执行任何操作。


我已根据https://stackoverflow.com/questions/14593/e ​​tiquette-for-modifying-posts 从原始问题中删除了版主评论

于 2008-08-26T09:01:30.970 回答
0

@基思:

IDisposable 用于托管资源。

终结者适用于非托管资源。

对不起,那是错误的。通常,终结器什么都不做。但是,如果dispose 模式已正确实现,则终结器会尝试调用Dispose.

Dispose有两个工作:

  • 释放非托管资源,以及
  • 释放嵌套的托管资源。

在这里,您的声明发挥了作用,因为确实在完成时,对象不应该尝试释放嵌套的托管资源,因为这些资源可能已经被释放。但它仍然必须释放非托管资源。

Dispose尽管如此,终结器除了调用并告诉它不要接触托管对象之外没有其他工作。Dispose,当手动(或通过Using)调用时,应释放所有非托管资源并将Dispose消息传递给嵌套对象(和基类方法),但这永远不会释放任何(托管)内存。

于 2008-08-26T09:11:17.703 回答
0

Konrad Rudolph - 是的,通常终结者什么都不做。除非您正在处理非托管资源,否则您不应该实现它。

然后,当你实现它时,你使用微软的 dispose 模式(如前所述)

  • public Dispose()调用protected Dispose(true)- 处理托管和非托管资源。调用Dispose()应该抑制最终确定。

  • ~Finalize调用protected Dispose(false)- 仅处理非托管资源。如果您未能调用public Dispose()

~Finalize速度很慢,除非您确实有非托管资源要处理,否则不应使用。

托管资源不会发生内存泄漏,它们只会为当前应用程序浪费资源并减慢其垃圾收集速度。非托管资源可能会泄漏,~Finalize最好的做法是确保它们不会泄漏。

在任何一种情况下using都是最佳实践。

于 2008-08-26T09:30:21.620 回答
0

在回答原始问题时,根据原始发布者迄今为止提供的信息,可以 100% 确定他对 .NET 编程的了解不够,甚至无法得到答案:使用 GC.Collect()。正如大多数海报所指出的那样,我会说他根本不需要使用 GC.Collect() 的可能性为 99.99%。

正确的答案归结为'让 GC 完成它的工作。时期。你还有其他事情要担心。但是您可能需要考虑是否以及何时应该处置或清理特定对象,以及是否需要在您的类中实现 IDisposable 和可能的 Finalize。

关于基思的帖子和他的规则#4:

有些海报混淆了规则 3 和规则 4。基思的规则 4 绝对正确,毫不含糊。这是四个规则中的一个,根本不需要编辑。我会稍微改写他的一些其他规则以使其更清晰,但如果您正确解析它们,它们基本上是正确的,并且实际上阅读了整篇文章以了解他如何扩展它们。

  1. 如果您的类不使用非托管资源并且它也从不实例化本身直接或最终使用非托管对象的类的另一个对象(即,实现 IDisposable 的类),那么您的类就不需要要么实现 IDisposable 本身,甚至调用 .dispose 任何东西。(在这种情况下,认为您实际上需要通过强制 GC 立即释放内存是愚蠢的。)

  2. 如果您的类使用非托管资源,或者实例化另一个本身实现 IDisposable 的对象,那么您的类应该:

    a) 在创建它们的本地环境中立即处置/释放它们,或者......

    b) 以 Keith 的帖子中推荐的模式,或互联网上的几千个地方,或现在大约 300 本书中推荐的模式实施 IDisposable。

    b.1) 此外,如果 (b),并且它是已打开的非托管资源,则 IDisposable 和 Finalize 都应该始终按照 Keith 的规则 #4 实施。
    在这种情况下,Finalize 在某种意义上绝对是一个安全网:如果有人实例化了使用非托管资源的您的 IDisposable 对象,并且他们未能调用 dispose,那么 Finalize 是您的对象正确关闭非托管资源的最后机会。
    (Finalize 应该通过调用 Dispose 来执行此操作,即 Dispose 方法跳过释放除非托管资源之外的任何内容。或者,如果您的对象的 Dispose 方法被任何实例化您的对象正确调用,那么它都将 Dispose 调用传递给它已实例化的所有 IDisposable 对象,并正确释放非托管资源,并以抑制对对象的 Finalize 的调用结束,这意味着如果调用者正确处置对象,则使用 Finalize 的影响会降低。所有这些点包含在基思的帖子中,顺便说一句。)

    b.2) 如果您的类只实现 IDisposable 因为它需要将 Dispose 传递给它已实例化的 IDisposable 对象,那么在这种情况下不要在您的类中实现 Finalize 方法。Finalize 用于处理任何实例化对象都未调用 BOTH Dispose 并且使用了仍未释放的非托管资源的情况。

总之,关于基思的帖子,他是完全正确的,在我看来,那个帖子是最正确和最完整的答案。他可能会使用一些有些人认为“错误”或反对的简短陈述,但他的完整帖子完全扩展了 Finalize 的用法,他是绝对正确的。在跳到他的帖子中的一条规则或初步声明之前,请务必完整阅读他的帖子。

于 2012-03-23T00:50:11.163 回答