【问题标题】:C# Winforms App - Large Objects stuck in gen 2 - Memory IssuesC# Winforms 应用程序 - 大对象卡在第 2 代 - 内存问题
【发布时间】:2011-08-19 22:09:17
【问题描述】:

我已经开始对我们的应用程序进行内存分析,因为我们最近收到了几份关于性能和内存不足异常的报告。该应用使用 C# .Net Winforms (.Net Framework 2.0)

开发

当应用程序启动时,ANT 分析器显示第 2 代中存在 17.7 MB 对象。

当应用程序启动时,它会从磁盘上的 xml 序列化文件中读取 77000 多个邮政编码,并保存在 Hashtable 中。请看下面的示例代码

public Class ZipCodeItem
{
    private string zipCode;
    private string city;
    private string state;
    private string county;
    private int tdhCode;
    private string fipsCounty;
    private string fipsCity;

    Public ZipCodeItem()
    {
         // Constructor.. nothing interesting here
    }

    // Bunch of public getter/setter properties
}

这是从磁盘上的文件中读取序列化的 zip 数据并加载邮政编码的静态类。

internal sealed class ZipTypes
{
    private static readonly Hashtable zipCodes = new Hashtable();

    public static ArrayList LookupZipCodes(string zipCode)
    {
        if (zipCodes.Count == 0)
            LoadZipCodes();

        ArrayList arZips = new ArrayList();

        // Search for given zip code and return the matched ZipCodeitem collection
        if (zipCodes.ContainsKey(zipCode))
        {
             // Populate the array with the matched items
        }

        // Omitted the details to keep it simple

        return arZips;
    }

    private static bool LoadZipCodes()
    {
        using (FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read))
        {
            // unzip it.. Omitted the details to keep it simple
            // Read the zipcodes from the flat xml file on disk and load the local zipCodes HashTable
        }
    }
}

这个类和更正。整个应用程序都可以访问邮政编码。

在 17.7 meg 的 Gen 2 对象中,大约有 14 meg 是 zipCodeItems 或其子 String 类。

我想将我的代码更改为如何不将这些 77000 多个邮政编码项目对象保留在内存中(在哈希表中),而是在应用需要时提供映射的邮政编码项目。

任何建议如何解决这个问题?提前致谢。

【问题讨论】:

  • 您认为 14MB 的 RAM 会导致 OOM 异常吗?它不是。有什么选择?每次需要邮政编码时从磁盘读取?不,根据我所见,将其存储在内存中没有任何问题。
  • 顺便说一句,您为什么使用ArrayListHashtable?这些类基本上已被弃用。首选 System.Collections.Generic 命名空间中的集合类,即 List<T>Dictionary<K,V>
  • Erm,您可以进行的另一项改进是使用TryGetValue 而不是Contains,然后执行另一次查找。你做的工作量是你需要做的两倍。
  • 是的,这很公平。但是,这 14MB 散列绝对不会导致 OOM 异常。您需要在代码/分析结果中查看其他地方。根据您告诉我们的内容,我可以向您保证,您走错了路。
  • 14MB 是现代应用程序的噪音。真的,我可以 99% 肯定地说这不是你的问题,而且你说的是 OOM 异常,这似乎更重要。您是否已证明您存在由 GC 引起的性能问题?

标签: c# winforms memory-management memory-leaks garbage-collection


【解决方案1】:

我将避免直接回答这个问题,希望能提供一个更有用的答案,因为我不相信与该哈希相关的 ~14MB 实际上会导致问题。

您说您正在使用 ANTS 内存分析器。这是一个很棒的工具,我以前也用过,所以也许我可以帮助你找出真正的问题。

我们可以放心地假设您的哈希不会导致OutOfMemoryException,因为它远不够大到足以这样做。保持原样,除了两个小改动:

  1. 使用强类型 Dictionary<K,V> 而不是 Hashtable。一旦 .NET 2.0 引入泛型,Hashtable 就基本上被弃用了。您也可以将 ArrayList 替换为 List<T>
  2. 不要执行Contains 然后在哈希中查找值,而是使用TryGetValue。这将哈希表查找的数量减少了一半。现在这可能不是您的应用的性能瓶颈,但我认为这也不等于过早优化。

现在,进入问题的关键......

您有分析器结果。回去看看你的内存分配在哪里。按顺序检查以下内容:

  1. .NET 占用了大部分内存还是本机代码(可能创建了很多实现 IDisposable 的对象,并且没有及时调用 Dispose()。)如果是后者,您可能知道在哪里去看看。
  2. 大对象堆 (LOH) 的外观如何?大部分内存是在那里分配的吗?许多大型分配可能会使 LOH 碎片化,并且可能在很长一段时间内都不会被压缩。 ANTS 会在结果概览页面的右上角告诉您。
  3. 事件处理程序。当对象订阅事件时,订阅者(通过 MultiCastDelegate,即事件对象)存储对订阅者(方法)的引用。这可能会导致对象的生命周期永远不会结束,并且在一段时间内,这可能会增加内存。您需要确保,如果正在创建对象然后超出范围,它还会取消订阅它之前订阅的任何事件。静态事件可能是这里的杀手锏。
  4. 使用 ANTS 来跟踪对象的生命周期。与上面类似,确保没有对象由于过时的引用而无意中保持活动状态。这可能比您想象的更容易发生。同样,查看创建了相对大量对象并超出范围的区域,以及其他对象维护对它们的引用的实例。 ANTS 可以在对象图中向您展示这一点。

这至少应该让您清楚地了解哪些内存被分配到哪里。您可能需要在分析器下运行您的程序一段时间并简单地观察内存使用情况。如果它稳步上升,那么您可以执行我上面列出的步骤,以尝试隔离哪些对象正在堆积。

【讨论】:

  • 请注意,事件处理程序 3 中的项目应该在内存分析器中显示为常规对象。解决它们有点特别,诊断则不然。
  • 是的,它们不会显示为任何特殊的东西,但是 ANTS 中的生命周期图(对象跟踪器?我忘记了名字...)会显示事件名称,所以它使事情有点更容易。
【解决方案2】:

您将需要某种存储以及一种简单的访问机制。

我建议使用某种形式的基于 SQL 的数据库。

我们在使用 SQL Compact Edition 方面取得了巨大成功,因为它可以在没有先决条件的情况下进行部署(也称为“私有部署”)。随后查询的集成故事非常紧凑 - 例如,使用 Linq to SQL 非常容易。

您还可以查看 SQL Lite 或其他提供程序。我会引导您远离 SQL Express 依赖项,因为它对于这些需求来说太重了。

此外,如果您现在将邮政编码缓存在数据库中,您还可以考虑查看某种“同步过程”(比如每天)来下载和解析您上面提到的输入 XML 文件(假设 XML 文件是从某个 Web 服务中检索的),否则您可以部署已经填充了数据的数据库。

【讨论】:

  • 我意识到我现在有点拖钓这个线程,但这似乎是一个疯狂的过度杀伤来替换一个绝对不会导致 OOM 异常的 14MB 哈希表。没有必要使用数据库,我认为 OP 在这里错过了实际问题。
  • 好吧,为什么不放弃整个 14MB 的静态内存使用呢?他们确实问过如何根本不存储在内存中。另外,考虑通过存储在更丰富的存储和查询引擎中来潜在地增加应用程序的功能 - 例如。邮政编码验证或基于其他地址组件的自动填充。
  • @Reddog:SQL 需要付出性能和内存占用的代价。如果没有明确规定它是最佳解决方案的要求,我不会添加它。
  • 是的,我不建议仅仅因为 OP 可能有一天需要它而添加数据库层。这给不需要它的应用程序增加了相当大的复杂性。
【解决方案3】:

如果您的用户抱怨内存不足异常,而您正在解决占用 15MB 内存的问题,那么您找错地方了。

我的其余答案假设 15MB 确实很重要。

话虽如此,我想提供一个替代已经提出的 SQL 解决方案(如果您确定确实不想将 15MB 加载到内存中,这些解决方案是很好的解决方案,具体取决于您的情况)。

我们将 3GB 的 IP 数据加载到我们网络服务器的进程空间中。但是,大多数 IP 地址大部分时间都不会被访问(特别是因为我们在用户群中有很强的地理偏见,但仍然需要为来自世界上不太常见的地区的访问者提供数据)。

为了保持较小的内存占用和非常快速的访问,我们使用了内存映射文件。虽然有 support for them built-in to .NET 4,但您仍然可以在任何以前的 .NET 版本中通过互操作调用来使用它们。

内存映射文件允许您将文件中的数据快速映射到进程空间的内存中。 SQL 为您在这种情况下可能不需要的东西(事务支持、键约束、表关系等)增加了开销……一般来说,所有很棒的功能,但对于解决这个问题是不必要的,并且在性能和内存占用方面付出了一些代价)。

【讨论】:

  • “我的其余答案假设 15MB 确实很重要” - 但事实并非如此。并且 MM 文件在这里无济于事。
【解决方案4】:

应用程序启动时,ANT 分析器显示 17.7 MB 对象 生活在第 2 代

我们遇到了类似的问题,我们发现可以将这些详细信息保存在内存中而不是多次加载。但它不应该在启动时加载。 更改逻辑以在第一次使用时按需加载。因为可能存在根本不使用的用例。对于这种情况,没有必要将该块保留在 Gen2 中。

我确信如果您遇到比这个更严重的内存泄漏问题 您的应用程序中存在内存问题。 ANT 探查器适用于 这种情况:-)

【讨论】:

    猜你喜欢
    • 2011-06-30
    • 2016-01-10
    • 2011-04-21
    • 1970-01-01
    • 2012-08-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-01
    • 1970-01-01
    相关资源
    最近更新 更多