为什么 System.Threading.OverlappedData 的垃圾回收需要这么长时间?

17

我正在通过内存分析器运行我的应用程序以检查泄漏。事情似乎还算可以,但是我得到了很多这些OverlappedData,它们似乎挂在终结器队列中无所作为。它们是被取消的重叠IO的结果,由于连接两端下层NetworkStream的关闭而产生。

网络流本身已经被处理。没有任何实例的NetworkStream存在。

通常,它们根源于被称为OverlappedDataCacheLine的东西。我在回调函数中调用EndRead作为我的第一件事,因此没有对应的BeginRead调用不应该出现。

这是一个典型的工具呈现它的样子

OverlappedData is being help up

最后,它会被GC,但要花费相当长的时间 - 大约半个小时才能杀死所有东西,当我启动了大约一千个流,将它们放入异步调用BeginRead中并在大约一分钟后关闭它们。

这个程序在80端口上针对web服务器部分地重现了这个问题。任何Web服务器都可以做到。

using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        var clients = new List<TcpClient>();
        for (int i = 0; i < 1000; ++i) {
            var client = new TcpClient();
            clients.Add(client);

            client.BeginConnect("localhost", 80, connectResult =>
                {
                    client.EndConnect(connectResult);

                    var stream = client.GetStream();
                    stream.BeginRead(new byte[1000], 0, 1000, result =>
                        {
                            try
                            {
                                stream.EndRead(result);
                                Console.WriteLine("Finished (should not happen)");
                            }
                            catch
                            {
                                // Expect to find an IO exception here
                                Console.WriteLine("Faulted");                               
                            }
                        }, stream);     
                }, client);             
        }

        Thread.Sleep(10000); // Make sure everything has time to connect

        foreach (var tcpClient in clients)
        {
            tcpClient.GetStream().Close();
            tcpClient.Close();
        }
        clients.Clear(); // Make sure the entire list can be GC'd

        Thread.Sleep(Timeout.Infinite); // Wait forever. See in profiler to see the overlapped IO take forever to go away
    }
}

虽然这个程序比真实应用程序小很多,因此不需要花费太长时间来清除数千个OverlappedData ,但它确实需要一段时间才能完成。当我运行真正的东西而不是测试应用程序时,会收到一个卡住了的终结器的警告。在我的应用程序中,它并没有做太多的事情,只是试图关闭可能没有被关闭的所有内容,并确保没有任何地方保留对任何内容的引用。

如果我调用客户端和它的流上的Dispose()Close()方法似乎根本没有关系。结果是一样的。

有什么线索可以解释为什么会出现这种情况以及如何避免这种情况吗?CLR是否对我进行智能处理,准备好新的调用来保持这些固定的内存块不变?为什么终结器完成起来如此之慢呢?

更新:经过一些非常愚蠢的负载测试(比如将一杯水放在F5键上并喝咖啡),似乎某些压力会触发更完整的GC来收集这些东西。因此实际上似乎没有真正的问题,但仍然很想知道到底发生了什么以及为什么收集这个对象的速度比其他对象慢几倍,如果在以后出现碎片化内存等问题是否可能成为问题。


未能调用EndXxxx会导致资源泄漏长达10分钟。 - Hans Passant
那么,如果在等待回调被调用时底层流已关闭,你是否可以以任何方式使用 EndXxx,考虑到除了回调之外没有什么东西在等待?我的意思是,如果事情出了差错,回调仍然会被调用,对吧? - Dervall
关闭套接字将完成操作。EndRead将清理这些资源并生成ObjectDisposed异常。只需确保在实际代码中仍调用EndRead即可。 - Hans Passant
@HansPassant 这基本上就是我所做的。我的意思是,它最终会被正确清理干净,只是需要很长时间才能完成。虽然不是大量的内存,但如果在服务器上承载了很多负载,就会有几千个这样的对象浮动。垃圾回收器似乎每隔一段时间就会挑选出一些对象,从我所看到的情况来看,它们的大小只有约120字节。当我停止负载时,没有10分钟的延迟直到开始处理这些对象。处理开始的时间尽可能早,但完成需要很长时间。其他对象的处理速度要快得多。 - Dervall
嗯,中止后GC实际上会运行多久?通常情况下,当您停止执行操作时,程序会停止分配内存。例如,当您没有活动连接时,也会停止收集。 - Hans Passant
@HansPassant 哦,您的意思是当我闲置等待连接时,GC运行得不那么频繁?这对我来说是个好消息,这也解释了为什么在负载期间我没有看到这些东西爆炸式增长,以及为什么在服务实际执行任务时收集似乎更加高效。我从来没有显式地调用过GC进行垃圾回收。 - Dervall
2个回答

6
好的,现在似乎清楚了正在发生什么。垃圾回收只会在分配内存时发生。需要至少2兆字节的分配,即第0代GC堆的典型初始大小才能触发GC。换句话说,一个什么也不做的程序永远不会运行GC,并且您将在内存分析器中看到尚未收集的任何对象长时间存在于堆中。
这是对您所描述的情况的很好的解释。在终止所有连接后,您的程序不必再执行任何操作。因此,不会分配太多或任何内存,因此不会触发收集。如果您的分析器没有显示收集,则可以使用Perfmon.exe查看它们。否则,这完全不是问题,只是垃圾回收器工作方式的副作用。
只有当您有明确证据表明程序存在失控的资源消耗问题时才需要担心泄漏。

谢谢!我测试了一下,结果没问题。我之前不知道这个行为。我有一个与此无关的内存泄漏,在分析应用程序以找到它时,这个问题出现了,我想知道这是否是原因。 - Dervall

0

尝试从 AsyncResult 中获取客户端,以排除任何 lambda 闭包问题。

TcpClient t = (TcpClient)connectResult.AsyncState;

此外,在完成处理之后,您不应该调用 EndConnect 吗?


所有实例都没有根据Closure进行处理,改用AsyncState也不会有任何变化。它们在终止队列中正确地浮动。应该在那里调用EndConnect,否则异步操作将无法完成。更改它并没有什么区别。 - Dervall

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