C#应用程序会自动释放托管资源吗?

7

我完全清楚使用using语句是处理IDisposable的方式。请不要在评论中重复这个建议。

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

我知道有些类有终结器来处理未托管的资源。

但是,假设我有一个控制台应用程序,其中包含一个静态的Stream变量。当我关闭控制台应用程序时,它会被处理吗?

HttpClient呢?你怎么知道它会在哪种情况下被处理,在哪种情况下不会被处理?

好了,现在是一些实际的背景信息。我经常将某些IDisposable作为字段存储,强制我的类实现IDisposable。最终用户应该使用using语句。但如果不这样做呢?

这只是不必要的内存直到GC?还是突然出现了内存泄漏?


简短回答:不是 - bommelding
4个回答

13
重要的是要区分实现了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 方面非常重要,但是当进程终止时,这不再是问题。

我认为当一个应用程序退出时,不会有任何东西被“收集”。那样做毫无意义。或许是已完成的。 - bommelding
是的,我也怀疑,但我不确定是否有两个代码片段可以找到最终化对象,可能只是要求GC运行完整周期以识别这些对象,但这远远超出了我的知识范围。 - Lasse V. Karlsen
具有终结器(~析构函数)的对象将保留在终结器队列中。无需进行内存扫描即可找到它们。这就是您使用 SuppressFinalize 将其从队列中移除的队列。 - bommelding
你是说它们在构造时被添加到队列中吗?我以为它们是作为GC的一部分被添加的。 - Lasse V. Karlsen
@LasseVågsætherKarlsen,这里有两个与终结相关的队列——终结队列存储所有具有终结器的对象列表(在创建此类对象时填充),而可回收队列则通过将引用从终结队列移动来填充(当对象通常被回收时)。 - Konrad Kokosa

4

没有任何东西会自动处理释放。

实现 IDisposable 接口的类是这样设计的,因为它们要么使用 IDisposable 字段(就像在您的情况下一样),要么使用非托管资源(有些例外情况不在本答案的范围内)。

CLR 中没有任何部分调用 Dispose 方法。
GC 将收集引用,并且除非另有指示(通过使用 GC.SuppressFinalize()),否则将引用移动到终结器队列中,在那里通过调用其终结器方法进行最终处理。
仅当类已显式重写终结器方法并在终结器方法中调用 Dispose 时,实例才会被处理。

因此,如果您想确保您的类由终结器处理,您必须重写类中的终结器方法。然而,请注意 - 正确实现终结器方法很难!

话虽如此,当您实现 IDisposable 接口时,您告诉使用此类的人应该处理它。无论他们实际上是否处理它,这已经不再是您的责任 - 而是他们的责任。因此,如果实际上存在内存泄漏(很可能会有一个),假设您的类正确实现了 IDisposable 接口,那就不是您的问题。


1
永远没有任何东西会自动被处理掉。 - Stefan
1
但是他们做过吗?曾经释放过foreach()或Thread的迭代器吗? - bommelding
1
IEnumeable 产生一个 Iterator<T> : IDisposable - bommelding
1
“因为C#编译器明确调用了它的Dispose方法”这句话的意思是某些东西会自动被处理掉,对吗? - bommelding
1
@ZoharPeled:我的观点是,即使非泛型的 IEnumerable 没有实现 IDisposable 接口,但如果对象实例是实现了该接口的类,那么 foreach 生成的代码将调用一个 IDisposable.Dispose 方法。并不是要说程序员应该在什么时候调用这个方法,而是提到一种情况,在这种情况下它会自动被调用,尽管它不明显。 - supercat
显示剩余6条评论

3
该进程即将消失。这意味着操作系统将停止提供任何内存来支持该进程的地址空间1。所有内存都将被“回收”。它还将终止该进程中的所有线程。因此,不会发生进一步的处理。所有句柄都将被关闭。
你需要担心的是外部现实。例如,如果您有一个未处理的Stream,如果它连接到网络或文件上的某些内容,它是否已经刷新了所有需要刷新的内容?
是的,如果您自己存储了可处置的对象,那么应该实现IDisposable。但是,如果没有任何非托管资源,则不要实现终结器。当终结器运行时,您不应该从该代码访问任何其他托管对象,因此太晚进行处理。
如果有人忘记处理您的对象,那是他们的错误,并且通过IDisposable接口广告您应该被处理的对象已经足够了。
1进程没有内存。它们有由操作系统管理的地址空间。在某些情况下,可能需要将该地址空间的某些部分保存在内存中。在虚拟内存时代,这是操作系统的工作,并且它只会将物理内存暂时借给任何给定的进程。它可以随时收回。

更好地解释“进程没有内存”的要点。 - Stefan

2
C#应用程序会自动释放托管资源吗?
嗯,是和不是,但从技术上讲:不是。
如果实现了IDisposable接口,当对象被回收时将调用dispose函数。如果没有正确实现,如果使用了非托管资源,就会出现泄漏。
当C# .NET 4.5(或更高版本)应用程序关闭时,未正确处理的IDisposables会发生什么?
进程空间被释放。如果IDisposable隐式地在其他进程空间中保留内存,并且未正确处理,则会发生泄漏。
但是假设我有一个控制台应用程序,其中包含一个静态流变量。当我关闭控制台应用程序时,它是否已被释放?
是的,虽然是隐式的,但可能会导致意外结果,例如当您杀死进程时。例如:TCP端口可能处于等待状态。
那么HttpClient呢?你怎么知道它在哪些情况下会释放,在哪些情况下不会?
与上述情况相同。
好的,现在是一些实际的背景信息。我经常将某些IDisposable对象存储为字段,强制我的类实现IDisposable接口。最终用户应该使用using语句来释放资源。但如果没有这样做怎么办?
你应该正确地实现IDisposable接口,并建议释放资源。例如文件句柄。如果它没有被正确释放,对文件的另一个调用可能会失败,因为文件仍然处于打开状态。这可能会在垃圾回收时解决,但你不知道何时会发生。
那么这只是无用的内存,直到垃圾回收吗?还是说你突然出现了内存泄漏?
它更像是泄漏,开放端口,打开句柄,视频内存消耗等等。
“对于正确实现IDisposable,请参见:正确使用IDisposable接口。”
“如果你读了它,你可以想象并不是所有的第三方库都正确地实现了它。”

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接