追踪 .NET Windows 服务内存泄漏

Posted

技术标签:

【中文标题】追踪 .NET Windows 服务内存泄漏【英文标题】:Tracking Down a .NET Windows Service Memory Leak 【发布时间】:2017-08-30 09:54:27 【问题描述】:

在生产中安装 Windows 服务之前,我一直在寻找可以执行的可靠测试,以确保我的代码不包含内存泄漏。 但是,我在网上能找到的只是使用任务管理器查看已用内存或一些付费的内存分析工具。

据我了解,查看任务管理器并没有太大帮助,也无法确认内存泄漏(以防万一)。

    如何确认是否存在内存泄漏?

    有没有免费的工具可以找到内存泄漏的根源?

注意:我正在使用 .Net Framework 4.6 和 Visual Studio 2015 社区

【问题讨论】:

这一切都取决于你的服务究竟做了什么。 @Evk 无论服务做什么,我如何确保服务中没有内存泄漏? 我的意思是很难真正测试程序的内存泄漏,尤其是 Windows 服务。您必须在一段时间内广泛地实际使用它。不断增长的内存本身并不表示泄漏,因为如果没有理由(没有内存压力),GC 可能会决定不收集任何东西。所以你能做的最好的就是编写没有泄漏的好代码,并且随着时间的推移监控应用程序的内存使用情况,如果内存达到某个阈值,则收集内存转储并使用分析器分析它们。 【参考方案1】:

虽然托管代码意味着没有直接的内存管理,但您仍然需要管理您的实例。这些实例“声称”内存。而这一切都与这些实例的使用有关,在您不希望它们存在时让它们保持活力。

只是众多示例之一:一次性类的错误使用会导致大量实例占用内存。对于 Windows 服务,缓慢但稳定地增加实例最终会导致大量内存使用。

是的,有一个分析内存泄漏的工具。它不是免费的。不过,您或许能够在 7 天试用期内发现您的问题。

我建议在.NET Memory Profiler 上抢夺。

在开发过程中分析内存泄漏非常棒。它使用快照的概念来比较新实例、已处置的实例等。这对了解您的服务如何使用其内存有很大帮助。然后,您可以更深入地了解为什么新实例会被创建或保持活动状态。

是的,您可以测试以确认是否引入了内存泄漏。 但是,开箱即用,这并不是很有用。这是因为没有人能够预料到运行时会发生什么。该工具可以分析您的应用程序的常见问题,但这不能保证。

但是,您可以使用此工具将内存消耗集成到您的单元测试框架中,例如 NUnitMSTest

【讨论】:

【参考方案2】:

你可以试试免费的 Memoscope 内存分析器

https://github.com/fremag/MemoScope.Net

我不同意您可以信任任务管理器来检查您是否有内存泄漏。垃圾收集器的问题在于它可以根据启发式方法决定在内存达到峰值后保留内存而不将其返回给操作系统。您可能有 2 GB 的提交大小,但其中 90% 可以是免费的。

您应该使用VMMAP 在测试期间检查您的进程包含什么类型的内存。您不仅有托管堆,还有非托管堆、私有字节、堆栈(线程泄漏)、共享文件等等需要跟踪。

VMMap 还具有命令行界面,可以定期创建快照,以便以后检查。如果您有内存增长,您可以根据泄漏类型不同的调试工具方法找出泄漏了哪种类型的内存。

【讨论】:

【参考方案3】:

当然,内存分析器是第一种尝试的工具,但它只会告诉您实例是否不断增加。您仍然想知道它们增加是否正常。此外,一旦您确定某些实例无缘无故地不断增加(意味着您有泄漏),您将想准确知道哪些调用树导致它们的分配,以便您可以对分配它们的代码进行故障排除和修复它,以便最终释放它们。

以下是我多年来在处理此类问题时收集的一些知识:

    尽可能将您的服务作为常规可执行文件进行测试。尝试将服务作为实际服务进行测试只会让事情变得过于复杂。

    养成在你正在做的事情范围结束时明确撤消你所做的一切的习惯。例如,如果您为某个被观察者的事件注册了一个观察者,则应该始终在某个时间点(观察者或被观察者的处置?)取消注册它。理论上,垃圾收集应该通过收集相互连接的观察者和被观察者的整个图来解决这个问题,但在实践中,如果你不改掉忘记撤消所做的事情的习惯,就会导致内存泄漏。

    尽可能使用IDisposable,如果有人忘记调用Dispose(),让你的析构函数报告。更多关于这个方法的信息在这里:Mandatory disposal vs. the "Dispose-disposing" abomination 披露:我是那篇文章的作者。

    1234563 /p>

    如果某个类的实例出现泄漏,请使用以下技巧找出导致它们分配的精确调用树:在该类的构造函数中,分配一个异常对象而不抛出它,获取堆栈跟踪的异常,并存储它。如果您稍后发现此对象已泄漏,则您有必要的堆栈跟踪。只是不要对太多对象执行此操作,因为分配异常并从中获取堆栈跟踪非常慢,只有 Microsoft 知道原因。

【讨论】:

【参考方案4】:

你可以使用任务管理器。 GC 应用程序可能会泄漏内存,它会在那里显示。

但是……

免费工具——“.Net CLR profiler”

有一个免费工具,它来自 Microsoft,非常棒。这是所有泄漏引用的程序都必须使用的。搜索 MS 的网站。

泄漏引用意味着您忘记将对象引用设置为 null,或者它们永远不会离开作用域,这在垃圾收集语言中发生的可能性几乎与未清除的语言一样 - 列表建立但未清除,事件处理程序指向委托等.

这是内存泄漏的 GC 等价物并且具有相同的结果。这个程序告诉你哪些引用占用了大量内存 - 你会知道它是否应该这样,如果不是,你可以找到它们并解决问题!

它甚至可以很酷地显示哪些对象分配了哪些内存(因此您可以跟踪错误)。如果您需要解释,我相信有这方面的 youtube。

Wikipedia page with download links...

注意:您可能必须将您的应用程序而不是作为服务运行才能使用它。它首先启动,然后运行您的应用程序。您可以使用 TopShelf 或仅将胆量放入一个 dll 中,该 dll 从实现服务集成(服务主机模式)的 EXE 运行。

【讨论】:

【参考方案5】:

如果您使用实体框架和 DI 模式,也许使用 Castle Windsor,您很容易出现内存泄漏。

要做的主要事情是尽可能使用 using( ) 语句来自动将对象标记为已释放。

此外,您希望在仅读取而不写入的实体框架上关闭自动跟踪。最好隔离您的写入,此时使用 using() ,获取 dbContext(启用跟踪),写入您的数据。

如果您想调查堆上的内容。我用过的最好的工具是 RedGate ANTS http://www.red-gate.com/products/dotnet-development/ants-memory-profiler/solving-memory-problems/getting-started 不便宜,但很管用。

但是,尽可能使用 using() 模式(不要创建静态或单例 DbContext,也不要在大规模更新循环中拥有一个上下文,尽可能频繁地处理它们! ) 那么你会发现内存通常不是问题。

希望这会有所帮助。

【讨论】:

【参考方案6】:

我不会说垃圾收集器是万无一失的。有时它会在不知不觉中失败,而且它们并不是那么直截了当。内存流是内存泄漏的常见原因。您可以在一个上下文中打开它们,它们甚至可能永远不会关闭,即使使用情况被包装在 using 语句中(一次性对象的定义应该在其使用超出范围后立即清理)。如果您因内存不足而遇到崩溃,Windows 会创建您可以筛选的转储文件。

enter link description here

这绝不是有趣或简单的,而且相当乏味,但它往往是你最好的选择。

容易造成内存泄漏的常见区域是使用 System.Drawing dll、内存流以及您正在执行一些严重的多线程的任何内容。

【讨论】:

【参考方案7】:

除非您处理的是非托管代码,否则我可以大胆地说您不必担心内存泄漏。托管代码中的任何未引用对象都将被垃圾收集器删除,并且在 .net 框架中发现内存泄漏的可能性我会说你应该被认为非常幸运(好吧,不幸)。您不必担心内存泄漏。

但是,如果从不释放对对象的引用,您仍然会遇到内存使用量不断增长的情况。例如,假设您保留了一个内部日志结构,并且您只是不断地将条目添加到日志列表中。那么每个条目仍然有来自日志列表的引用,因此永远不会被收集。

根据我的经验,您绝对可以使用任务管理器作为您的系统是否存在日益严重的问题的指标;如果内存使用率持续上升,你就知道你有问题。如果它增长到一个点但最终收敛到某个大小,则表明它已达到其操作阈值。

如果您想更详细地了解托管内存使用情况,可以下载 Microsoft 开发的进程资源管理器here。它仍然很生硬,但它提供了比任务管理器更好的统计视图。

【讨论】:

这是一个很好的答案。它回答了您的第一个问题:您可以使用任务管理器来观察内存使用行为。如果内存使用量从未下降,则表明您有问题。根据我的经验,这很有效。不需要其他工具。关键是垃圾收集器只删除未引用的对象,因此请确保对对象的所有引用在完成时都被销毁。在整个服务中实施 Dispose 模式。在可用时调用 Dispose 并将大对象设置为 null。这是防止内存问题的最佳方法。 @Micael 这不是真的。内存泄漏也可能是由托管代码引起的。想想未释放的对象、委托订阅、弱引用…… 未处理的对象最终会被垃圾收集器处理掉。我想它归结为定义。内存泄漏通常是指没有被释放的内存块,但没有任何东西涉及它。在每个块引用另一个块的情况下链接内存块不被视为内存泄漏,而是一种低效的结构;您不希望垃圾收集器释放内存,因为您实际上可能有理由保持这种状态。它是内存吞噬者,但不是内存泄漏。

以上是关于追踪 .NET Windows 服务内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

内存泄漏与垃圾回收机制

如何在 Jboss AS 5.1 中追踪非堆 JVM 内存泄漏?

内存泄漏问题,私有数据正在增加.Net Framework 4

分析 ThreadLocal 内存泄漏问题

内存泄漏和内存溢出的区别

Android开发常见的Activity中内存泄漏及解决办法