【问题标题】:Does C# app exit automatically dispose managed resources?C# 应用程序退出是否会自动释放托管资源?
【发布时间】:2019-03-09 16:32:19
【问题描述】:

我完全清楚 using 语句是处理 IDisposables 的方式。请不要在 cmets 中重复此建议。

当 C# .NET 4.5(或更高版本)应用程序关闭时,未正确处理的 IDisposable 会发生什么情况?

我知道有些人有一个终结器来处理非托管资源。

但是假设我有一个带有静态 Stream 变量的控制台应用程序。当我关闭控制台应用程序时,它会被释放吗?

HttpClient 呢?你怎么知道它在哪些情况下可以,在哪些情况下不可以?

好的,现在是一些实际的背景信息。我经常将某些 IDisposable 存储为字段,迫使我的班级实现 IDisposable。最终用户应该使用 using。但如果这没有发生呢?

在 GC 之前它只是不必要的内存吗?还是突然出现内存泄漏?

【问题讨论】:

  • 这是几道题? ;-)
  • 简答:no

标签: c# memory-management garbage-collection exit idisposable


【解决方案1】:

C# 应用退出是否会自动释放托管资源?

嗯,是也不是,但从技术上讲:不是。

如果IDisposable接口正确实现,则在收集对象时会调用dispose函数。如果没有正确实现,如果使用非托管资源就会出现泄漏。

当 C# .NET 4.5(或更高版本)应用程序关闭时,未正确处理的 IDisposable 会发生什么情况?

进程空间被释放。如果IDisposable 隐式地在其他进程空间中保留内存,并且没有正确处理:你有泄漏。

但是假设我有一个带有静态 Stream 变量的控制台应用程序。当我关闭控制台应用程序时它会被释放吗?

虽然是隐含的,但它可能会导致意外结果,例如当您终止进程时。例如:一个 TCP 端口可能处于等待状态。

HttpClient 呢?你怎么知道它在哪些情况下可以,在哪些情况下不可以?

同上。

好的,现在是一些实际的背景信息。我经常将某些 IDisposable 存储为字段,迫使我的班级实现 IDisposable。最终用户应该使用 using。但如果这没有发生呢?

您应该正确实施它,但建议丢弃。例如文件句柄。如果未正确处理,则对该文件的另一次调用可能会失败,因为该文件仍处于打开状态。这可能会在 GC 上解决,但你不知道什么时候。

在 GC 之前它只是不必要的内存吗?还是突然出现内存泄漏?

更多,泄漏,打开端口,打开句柄,视频内存消耗等。


有关IDisposable 的正确实现,请参阅:Proper use of the IDisposable interface

如果您阅读它,您可以想象并非所有第 3 方库都正确实现它。

【讨论】:

    【解决方案2】:

    进程即将消失。这意味着操作系统将停止提供任何内存来支持该进程的地址空间1。无论如何,所有内存都被“回收”了。它还将终止进程中的所有线程。所以不会发生进一步的处理。无论如何,所有句柄都将关闭。

    您需要担心的是外部现实。例如。如果你有一个未处理的Stream,如果它通过网络或文件附加到某个东西,它是否刷新了它需要的所有东西?

    是的,如果你自己存储Disposables,你应该实现IDisposable。但是,如果您没有任何 非托管 资源,请不要实现终结器。到终结器运行时,您不应该从该代码访问任何其他 托管 对象,因此处理它们为时已晚。

    如果有人忘记 Dispose 您的对象,那是他们的错误,而您已经做了应有的努力,通过IDisposable 界面宣传您应该被释放。


    1进程没有内存。它们有一个由操作系统管理的地址空间。有时,该地址空间的某些部分可能需要在内存中。这是操作系统在虚拟内存时代的工作,它只是将物理内存临时“借出”给任何给定进程。它可以随时再次将其带走

    【讨论】:

    • 更好地解释“进程没有内存”的要点。
    【解决方案3】:

    永远不会自动处置任何东西。

    实现 IDisposable 接口的类是这样设计的,因为它们要么使用 IDisposable 字段(如您的情况)或使用非托管资源(此规则有例外,但这超出了此答案的范围)。

    CLR 中没有调用Dispose 方法的部分。
    GC 将收集引用,除非另有指示(通过使用GC.SuppressFinalize(),然后将引用移动到终结器队列,其中 它将通过调用它的 finalize 方法来完成。
    当且仅当该类已显式覆盖 finalize 方法并在 finalize 方法中调用 Dispose 时,该实例才会最终被释放。

    所以,如果你想确保你的类被终结器处理,你必须override the finalize method in your class。但是,请注意 - Implementing the Finalize method correctly is hard!

    话虽如此,当你实现IDisposable 接口时,你是在告诉任何使用这个类的人,它应该被释放。他们是否真的处理它不再是你的责任 - 这是他们的。因此,如果确实存在内存泄漏(而且很有可能会有一个) - 假设您的班级 implemented the IDisposable interface correctly,那不是您的问题。

    【讨论】:

    • 点数“永远不会自动处理。”
    • 但他们确实... 曾经处理过 foreach() 的迭代器吗?还是线程?
    • @bommelding System.Threading.Thread 没有实现IDisposable 接口,至于foreach的迭代器,我不确定你的意思。
    • IEnumeable 产生一个Iterator<T> : IDisposable
    • “因为 c# 编译器显式调用它的 Dispose”不是意味着某些东西会自动处理吗?
    【解决方案4】:

    区分实现IDisposable 的对象和带有终结器的对象很重要。在大多数情况下(可能最好全部),带有终结器的对象也实现了IDisposable,但它们实际上是两个不同的东西,最常一起使用。

    终结器是一种对 .NET 运行时说,在它收集对象之前,它必须执行终结器的机制。当 .NET 运行时检测到对象符合垃圾回收条件时,就会发生这种情况。通常,如果对象没有终结器,它将在此收集期间被收集。如果它有一个终结器,它将被放置到一个列表中,即“freachable queue”,并且有一个后台线程监控这个线程。有时在集合将对象放入此队列后,终结器线程会处理此队列中的对象并调用终结器方法。

    一旦发生这种情况,该对象再次符合回收条件,但它也被标记为已完成,这意味着当垃圾回收器在未来的回收周期中找到该对象时,它不再将其放入此队列,而是正常收集。

    请注意,在上面的文字段落中,IDisposable 没有被提及一次,这是有充分理由的。以上都不依赖IDisposable根本

    现在,实现IDisposable 的对象可能有也可能没有终结器。一般规则是,如果对象本身拥有非托管资源,它可能应该拥有,如果不拥有,则可能不应该拥有。 (我很犹豫要不要说永远和永远不会在这里,因为似乎总是有人能够找到一个角落案例,它以某种方式有意义但违反了“典型”规则) p>

    TL;DR 对上述内容的总结可能是,终结器是一种在收集对象时获得(半)保证清理对象的方法,但确切的时间不是直接在程序员的控制之下,而实现IDisposable 是一种直接从代码中控制这种清理的方法。

    无论如何,让我们来解决您的具体问题:

    当 C# .NET 4.5(或更高版本)应用程序关闭时,未正确处理的 IDisposable 会发生什么情况?

    回答:什么都没有。如果它们有终结器,终结器线程将尝试拾取它们,因为当程序终止时,所有对象都可以被收集。但是,终结器线程不允许“永远”运行来执行此操作,因此它也可能会超时。另一方面,如果实现 IDisposable 的对象没有终结器,它只会被正常收集(同样,IDisposable 与垃圾收集完全无关)。

    但是假设我有一个带有静态 Stream 变量的控制台应用程序。当我关闭控制台应用程序时它会被释放吗?

    回答:不,不会处理Stream 本身是一个基类,因此取决于具体的派生类,它可能有也可能没有终结器。但是,它遵循与上述相同的规则,因此如果它没有终结器,它将被简单地收集。例如,MemoryStream 没有终结器,而FileStream 有。

    HttpClient 呢?你怎么知道它在哪些情况下可以,在哪些情况下不可以

    答案:reference source for HttpClient 似乎表明HttpClient 没有终结器。因此它会被简单地收集起来。

    好的,现在是一些实际的背景信息。我经常将某些 IDisposable 存储为字段,迫使我的班级实现 IDisposable。最终用户应该使用 using。但如果这没有发生呢?

    答案:如果您忘记/不调用实现IDisposable 的对象上的IDisposable.Dispose(),那么一旦对象符合收集条件,我在此处所述的有关终结器的所有内容仍然会发生。除此之外,不会发生什么特别的事情。对象是否实现IDisposable 与垃圾收集过程无关,只有终结器的存在。

    在 GC 之前它只是不必要的内存吗?还是你突然出现内存泄漏

    答案:根据这个简单的信息无法确定。这取决于Dispose 方法会做什么。例如,如果对象已经在某处注册了自己,因此在某处存在对它的引用,那么某些代码停止使用该对象可能实际上不会使该对象符合收集条件。 Dispose 方法可能负责注销它,删除对它的最后引用。所以这取决于对象。仅仅对象实现IDisposable 的事实不会造成内存泄漏。如果删除了对该对象的最后一个引用,则该对象有资格被收集,并将在未来的收集周期中被收集。


    备注:

    • 请注意,上面的文字也可能是简化的。在应用程序终止时可能不会完成实际“收集内存”的完整收集周期,因为没有意义。无论如何,操作系统都会在进程终止时释放进程分配的内存。可能只执行最终确定。 (意思是,我不知道这里做了什么样的优化)

    • 这里更重要的部分是您需要区分程序执行期间和程序执行后的内存(或其他)泄漏

      • 当进程终止时,操作系统将回收分配给它的所有内存,它会关闭所有句柄(这可能会保持套接字、文件等打开),所有线程都将终止。简而言之,程序完全从内存中删除
      • 该进程可能已经留下了自己的花絮,除非该进程事先注意这样做,否则不会清理这些花絮。如上所述,打开的文件已关闭,但它可能尚未完全写入,因此可能以某种方式损坏。
      • 在程序执行期间,泄漏可能使程序在分配的内存方面增长,它可能分配过多的句柄,因为它无法关闭不再需要的句柄,等等,这对于处理IDisposable 而言很重要和终结器正确,但是当进程终止时,这不再是问题。

    【讨论】:

    • 我认为应用程序退出时不会“收集”任何东西。那将是毫无意义的。可能已经敲定了。
    • 是的,我也对此表示怀疑,但我不确定是否有两段代码也可以找到要完成的对象,可能他们只是要求 GC 运行一个完整的循环来识别这些对象,但这远远超出了我的知识范围。
    • 具有终结器 (~destructor) 的对象保留在终结器队列中。无需进行内存扫描即可找到它们。这就是您使用 SuppressFinalize 将他们踢出的队列。
    • 您是说它们在构造时被添加到该队列中吗?我以为它们是作为 GC 的一部分添加的?
    • @LasseVågsætherKarlsen 有两个与终结相关的队列 - 终结队列存储所有对象的列表,其中包含终结器(在创建此类对象时填充)和可访问队列(通过从终结队列中移动引用来填充对象通常会被收集)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-27
    • 1970-01-01
    • 2011-01-13
    • 1970-01-01
    • 2020-02-15
    相关资源
    最近更新 更多