排查 ASP.NET 中的 System.OutOfMemoryException) (内存不足问题

本文可帮助你排查 ASP.NET 中的 内存不足 错误。

原始产品版本: ASP.NET
原始 KB 编号: 2020006

症状

我们在 Microsoft 客户支持服务中看到的最常见问题之一是 OutOfMemoryException 方案。 因此,我们汇集了一系列资源,以帮助排查和确定内存问题的原因。

在介绍故障排除 OutOfMemoryException的详细信息之前,请务必先了解导致此问题的原因。 与许多开发人员所认为的相反,安装的 RAM 量不会影响 OutOfMemoryException的可能性。 32 位操作系统可以寻址 4 GB 的虚拟地址空间,无论在框中安装多少物理内存。 其中,为操作系统保留 2 GB (内核模式内存) ,2 GB 分配给用户模式进程。 为内核模式内存分配的 2 GB 在所有进程之间共享,但每个进程获取其自己的 2 GB 用户模式地址空间。 这一切都假定你未在启用开关的情况下 /3gb 运行。

当应用程序需要使用内存时,它会保留虚拟地址空间的一个区块,然后从该区块提交内存。 这正是.NET Framework的垃圾回收器 (GC) 在需要内存来增加托管堆时所执行的操作。 当 GC 需要一个新的小对象堆段 (小于 85 KB 的对象驻留) 时,它会分配 64 MB。 当它需要为大型对象堆使用新段时,它会分配 16 MB。 这些大型分配必须从进程必须处理的 2 GB 地址空间的连续块中完成。 如果操作系统无法满足 GC 对连续内存块的请求,则 System.OutOfMemoryException 会发生 (OOM) 。

注意

在 64 位操作系统上运行的 32 位进程可以处理 4GB 用户模式内存,在 64 位操作系统上运行的 64 位进程可以处理 8TB 的用户模式内存,因此 OOM 在 64 位操作系统上不太可能。 可以在 64 位操作系统上运行的 32 位进程中体验 OOM,但通常不会发生,直到进程使用接近 3 GB 的专用字节。

出现 OOM 条件的原因有两个。

  1. 在 32 位环境中,进程使用大量内存 (通常超过 800 MB。)
  2. 虚拟地址空间是碎片的,降低了大型连续分配成功的可能性。

还可以看到 OOM 条件,因为 1 和 2 的组合。 有关详细信息,请参阅 ASP.NET 中的 System.OutOfMemoryExceptions 故障排除

发生 OOM 时,你可能会注意到以下一个或多个症状:

在确定 OOM 条件的原因时,你实际上是在确定内存过高或地址空间碎片的原因。 虽然我们无法记录这些情况的所有可能原因,但我们经常看到一些常见原因。

以下信息概述了 OOM 条件的常见原因以及解决其中每个原因的解决方法。

字符串连接

托管应用程序中的字符串 (使用.NET Framework) 编写的应用程序是不可变的。 将新值分配给字符串时,将创建现有字符串的副本。 新值将分配给新字符串。 它通常不会导致任何问题。 但是,当大量字符串串联时,最终会导致比开发人员可能意识到的更多的字符串分配。 它可能导致内存增长和 OOM 条件。

若要避免由于字符串串联而出现 OOM,请确保使用 StringBuilder 类。 有关详细信息,请参阅 如何提高 Visual C# 中的字符串串联性能

托管堆中的碎片

托管应用程序中的垃圾回收器 (GC) 压缩堆以减少碎片量。 但是,可以在托管应用程序中固定对象。 在堆压缩期间无法移动固定的对象。 这样做会更改对象所在的地址。 如果应用程序固定大量对象和/或长时间固定对象,则可能会导致托管堆中的碎片。 这可能会导致 GC 更频繁地增加托管堆,并导致 OOM 条件。

自 .NET Framework 1.0 以来,我们一直在努力最大程度地减少 OOM 条件,因为固定。 每个版本都进行了增量改进。 但是,如果你需要固定对象,仍然可以实现一些设计模式,这些模式将非常有用。

虚拟地址 (VA) 空间中的碎片

每个进程都有一定数量的内存分配给它,该内存表示进程的 VA 空间。 如果 VA 空间碎片化,则增加 GC 无法获取大块连续内存来增加托管堆的可能性。 它可能导致 OOM 条件。

VA 空间中的碎片通常由以下一种或多种方案引起:

  • 将相同的程序集加载到多个应用程序域中。

    如果需要在同一应用程序池中运行的多个应用程序中使用程序集,请对程序集进行强名称并将其安装到 GAC 中。 通过执行此操作,可以确保程序集只加载到进程中一次。

  • 在生产环境中运行应用程序, <compilation> 并将 元素的 debug 属性设置为 true

    • 元素的 <compilation> debug 属性应在 false 生产环境中。
    • 可以使用配置 <deploy retail="true" /> 来确保始终在产品中禁用调试。 有关详细信息,请参阅 deployment Element (ASP.NET Settings Schema)
  • 在可扩展样式表语言 (中使用脚本) 转换或创建 XmlSerializers

    在这种情况下,由可扩展样式表语言转换引起的动态程序集 (XSLT) 脚本或 XmlSerializers

返回大型数据集

使用来自数据库或其他数据源的数据时,请务必限制返回的数据量。 例如,缓存返回整个数据库表的查询结果以避免在需要时从数据库检索部分数据的成本不是一种好方法。 这样做很容易导致内存过高,并导致 OOM 条件。 允许用户启动类似的查询是创建高内存情况的另一种常见方法。 例如,返回公司中的所有员工或德克萨斯州的所有客户,姓氏以字母 S 开头。

始终限制可从数据库返回的数据量。 不允许这样的 SELECT * FROM. . . 查询,因为你无法控制页面中显示的数据量。

同样重要的是,确保不会在 UI 元素(如 GridView 控件)中显示大数据结果。 除了返回的数据所需的内存外,还将在呈现结果所需的字符串和 UI 元素中使用大量数据。 通过实现分页和验证输入以便不返回大型数据集,可以避免此问题。

在启用了跟踪的生产环境中运行

ASP.NET 跟踪是用于对应用程序进行故障排除的强大功能。 但它绝不应在生产环境中保留。 ASP.NET 跟踪使用数据结构(例如 DataTables )来存储跟踪信息,并且随着时间的推移,它们可能会导致高内存状况,从而导致 OOM。

应在生产环境中禁用跟踪。 为此,可以在 web.config 文件中将 元素的 <trace> 属性设置为 enabled false。 使用 <deploy retail="true" /> 启用零售部署还会禁用应用程序中的跟踪。

泄漏本机资源

许多托管资源也会使用本机资源。 由于 GC 不会清理本机资源,因此开发人员负责实现并调用 Dispose 方法来清理本机资源。 如果使用实现 IDisposable 接口的类型,并且未调用 Dispose 方法,则存在泄露本机资源并导致 OOM 条件的风险。

这些对象应实现 iDisposable 接口,并且当不再需要这些对象时,应对这些对象调用 Dispose 方法。