重要的是要区分实现了
IDisposable
接口和有终结器(finalizer)的对象。在大多数情况下(可能是所有情况),有终结器的对象也实现了
IDisposable
接口,但它们实际上是两个不同的东西,通常一起使用。
终结器是一种机制,用于告诉.NET运行时,在它可以收集对象之前,必须执行终结器。这发生在.NET运行时检测到对象符合垃圾回收的条件时。通常,如果对象没有终结器,它将在此收集期间被收集。如果它有一个终结器,它将被放置在一个列表上,即“freachable queue”,并且有一个后台线程监视此线程。有时,在收集将对象置于此队列后,终结器线程将从此队列处理对象并调用终结器方法。
一旦发生这种情况,对象再次符合收集条件,但它也已被标记为已终结,这意味着当垃圾回收器在将来的收集周期中找到该对象时,它不再将其放置在此队列上,而是正常收集它。
请注意,在上述文本段落中,
IDisposable
没有被提及一次,这是有很好的原因的。上述所有内容都不涉及
IDisposable
。
现在,实现
IDisposable
接口的对象可能会有终结器,也可能没有。一般规则是,如果对象本身拥有非托管资源,则可能应该具有终结器,否则可能不应该具有终结器。(我在这里犹豫地说“总是”和“从不”,因为似乎总有人能够找到一个角落案例,在这种情况下它是有意义的,但违反了“通常”的规则)
以上内容中没有提到
IDisposable
一次,这是有很好的原因的。以上所有内容都与垃圾回收有关,根本不涉及
IDisposable
。
简而言之,终结器是可以在对象被收集时对其进行(半)保证清理的一种方法,但直接控制何时发生这种情况并不在程序员的控制范围内,而实现
IDisposable
接口是一种直接从代码控制此清理的方式。
无论如何,在我们掌握了所有这些知识后,让我们来回答您的具体问题:
当C# .NET 4.5(或更高版本)应用程序关闭时,未正确处理的IDisposables会发生什么情况?
答案:什么都不会发生。如果它们有终结器,终结器线程将尝试获取它们,因为当程序终止时,所有对象都变为符合收集条件。然而,终结器线程不能永远运行以完成此操作,因此它也可能会超时。另一方面,如果实现
IDisposable
接口的对象没有终结器,则它将被普通方式收集(再次强调,
IDisposable
与垃圾回收无关)。
但是,假设我有一个控制台应用程序,并具有静态的Stream变量。当我关闭控制台应用程序时,它会被处理吗?
答案: 不会被“处理”。Stream
本身是一个基类,因此根据具体的派生类,它可能有也可能没有finalizer。它遵循与上述相同的规则,因此如果它没有finalizer,它将被简单地收集。例如,MemoryStream没有finalizer,而FileStream有。
那HttpClient呢?你怎么知道在哪种情况下它有或者没有?
答案:HttpClient的参考源代码似乎表明HttpClient
没有finalizer。因此,它只会被收集。
好吧,现在说一些实际的背景信息。我经常将某些IDisposables存储为字段,强制我的类实现IDisposable接口。最终用户应该使用using。但如果没有这样做呢?
答案: 如果您忘记/未调用实现IDisposable
接口对象的IDisposable.Dispose()
方法,那么一旦该对象有资格进行收集,所有我在此处所述关于finalizer的内容仍然会发生。除此之外,没有特别的事情会发生。无论对象是否实现了IDisposable
接口,都不会影响垃圾收集过程,只有finalizer的存在才会有影响。
这只是无用的内存,直到GC吗?还是突然出现内存泄漏?
答案: 从这个简单的信息中还不能确定。这取决于Dispose
方法会做什么。例如,如果对象在某处注册了自己以便有一个引用指向它,则停止使用该对象可能实际上并不使该对象有资格进行收集。 Dispose
方法可能负责注销它,删除最后的引用。因此,这取决于对象。仅仅因为对象实现了IDisposable
接口并不会导致内存泄漏。如果对该对象的最后一个引用被移除,则该对象变为有资格进行收集,并将在未来的垃圾回收周期中进行收集。
备注:
注意,上述文本可能也是简化的。实际上,“收集内存”的完整收集周期可能在应用程序终止时并没有执行,因为这毫无意义。操作系统在进程终止时会释放分配给该进程的内存。当应用程序终止时,.NET Framework 尽最大努力调用尚未进行垃圾回收的对象的 finalizer,除非已经抑制了这种清理(例如通过调用库方法 GC.SuppressFinalize)。 .NET 5(包括.NET Core)和更高版本不会在应用程序终止时调用 finalizer。
1(我不知道是否做过任何优化)
更重要的是,你需要区分程序执行期间和程序执行后的内存(或其他)泄漏。
- 当进程终止时,操作系统将回收分配给它的所有内存,关闭所有句柄(可能保持套接字、文件等打开状态),所有线程都将终止。简而言之,程序将完全从内存中删除。
- 然而,进程可能会留下自己的一些残留物,在进程事先没有进行清理的情况下,这些残留物不会被清除。一个打开的文件将被关闭,如上所述,但它可能没有被完全写入,因此可能以某种方式损坏。
- 在程序执行期间,泄漏可能使程序在分配的内存方面增长,可能会分配太多的句柄,因为它未能关闭不再需要的句柄等等,这在正确处理 IDisposable 和 finalizer 方面非常重要,但是当进程终止时,这不再是问题。