【问题标题】:XDocument + IEnumerable is causing out of memory exception in System.Xml.Linq.dllXDocument + IEnumerable 导致 System.Xml.Linq.dll 内存不足异常
【发布时间】:2011-05-26 01:16:11
【问题描述】:

基本上我有一个程序,它在启动时加载文件列表(如FileInfo),并为列表中的每个文件加载一个XML文档(如XDocument)。

然后程序将其中的数据读入容器类(存储为IEnumerables),此时XDocument 超出范围。

然后程序将数据从容器类导出到数据库。但是,在导出容器类超出范围后,垃圾收集器没有清理容器类,因为它存储为IEnumerable,似乎导致XDocument 留在内存中(不确定这是否是原因,但任务管理器显示来自XDocument 的内存没有被释放)。

由于程序循环遍历多个文件,最终程序会抛出内存不足异常。为了缓解这种情况,我最终使用了

System.GC.Collect(); 

在容器超出范围后强制垃圾收集器运行。这是有效的,但我的问题是:

  • 这是正确的做法吗? (强制垃圾收集器运行似乎有点奇怪)
  • 有没有更好的方法来确保XDocument 内存正在被释放?
  • 除了 IEnumerable 之外,是否还有其他原因导致文档内存未被释放?

谢谢。


编辑:代码示例:

  • 容器类:

    public IEnumerable<CustomClassOne> CustomClassOne { get; set; }
    public IEnumerable<CustomClassTwo> CustomClassTwo { get; set; }
    public IEnumerable<CustomClassThree> CustomClassThree { get; set; }
    ...
    public IEnumerable<CustomClassNine> CustomClassNine { get; set; }
    
  • 自定义类:

    public long VariableOne { get; set; }
    public int VariableTwo { get; set; }
    public DateTime VariableThree { get; set; }
    ...
    

    无论如何,这确实是基本结构。自定义类通过 XML 文档中的容器类填充。填充的结构本身使用的内存非常少。

一个容器类从一个 XML 文档填充,超出范围,然后加载下一个文档,例如

    public static void ExportAll(IEnumerable<FileInfo> files)
    {
        foreach (FileInfo file in files)
        {
            ExportFile(file);
            //Temporary to clear memory
            System.GC.Collect();
        }
    }
    private static void ExportFile(FileInfo file)
    {
        ContainerClass containerClass = Reader.ReadXMLDocument(file);
        ExportContainerClass(containerClass);
        //Export simply dumps the data from the container class into a database
        //Container Class (and any passed container classes) goes out of scope at end of export
    }

    public static ContainerClass ReadXMLDocument(FileInfo fileToRead)
    {
        XDocument document = GetXDocument(fileToRead);
        var containerClass = new ContainerClass();

        //ForEach customClass in containerClass
        //Read all data for customClass from XDocument

        return containerClass;
    }

忘了提这个位(不确定是否相关),文件可以压缩为.gz,所以我有GetXDocument()方法来加载它

    private static XDocument GetXDocument(FileInfo fileToRead)
    {
        XDocument document;

        using (FileStream fileStream = new FileStream(fileToRead.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            if (String.Equals(fileToRead.Extension, ".gz", StringComparison.OrdinalIgnoreCase))
            {
                using (GZipStream zipStream = new GZipStream(fileStream, CompressionMode.Decompress))
                {
                    document = XDocument.Load(zipStream);
                }
            }
            else
            {
                document = XDocument.Load(fileStream);
            }
            return document;
        }
    }

希望这是足够的信息。 谢谢

编辑:System.GC.Collect() 没有 100% 的时间工作,有时程序似乎保留了XDocument,有人知道为什么会这样吗?

public static ContainerClass ReadXMLDocument(FileInfo fileToRead)
{
    XDocument document = GetXDocument(fileToRead);
    var containerClass = new ContainerClass();

    //ForEach customClass in containerClass
    //Read all data for customClass from XDocument

    containerClass.CustomClassOne = document.Descendants(ElementName)
        .DescendantsAndSelf(ElementChildName)
        .Select(a => ExtractDetails(a));

    return containerClass;
}

private static CustomClassOne ExtractDetails(XElement itemElement)
{
    var customClassOne = new CustomClassOne();
    customClassOne.VariableOne = Int64.Parse(itemElement.Attribute("id").Value.Substring(4));
    customClassOne.VariableTwo = int.Parse(itemElement.Element(osgb + "version").Value);
    customClassOne.VariableThree = DateTime.ParseExact(itemElement.Element(osgb + "versionDate").Value,
            "yyyy-MM-dd", CultureInfo.InvariantCulture);
    return customClassOne;
}

【问题讨论】:

  • 我建议不要手动调用 GC。我们可以看一些示例代码,以便我们了解它在做什么吗?
  • 我建议您认为事情不会超出范围。您的 IEnumerable 将引用(以支持延迟评估)您不再直接在其他地方引用的内容。只要您引用 IEnumerable,它枚举的所有内容都仍然存在。
  • @Matthew:明天将添加一些示例代码,因为我目前无法访问它。 @Colin:如果我认为手动调用垃圾收集器时它没有超出范围,肯定不会做任何事情?
  • @Manatherin:另外,请记住 IEnumerable&lt;T&gt; 继承 IDisposable,因此您可能希望在这些对象上调用 Dispose,或者将它们放入 using 块中。跨度>
  • @John:实际上IEnumerable&lt;T&gt; 没有实现IDisposable,但IEnumerator&lt;T&gt; 实现了。除非他直接调用 GetEnumerator,否则没有什么可处置的。

标签: c# linq garbage-collection linq-to-xml out-of-memory


【解决方案1】:

在某些情况下,强制手动垃圾回收似乎解决了您的问题,但可以肯定的是,这不过是巧合。

您需要做的是停止猜测导致您的内存压力问题的原因,而是要确定。

我在类似情况下使用JetBrains dotTrace 效果非常好 - 设置断点,触发分析器并浏览所有“活动”对象及其关系的视图。可以轻松找到哪些对象仍然保留,以及它们通过哪些引用保持活动状态。

虽然我自己没有使用过,但RedGate Ants Memory Profiler 也受到很多人的推荐。

这两个工具都有免费试用版,应该足以解决您当前的问题。不过,我强烈建议值得购买其中一个 - dotTrace 为我节省了数十小时的内存问题故障排除时间,这是非常值得的投资回报率。

【讨论】:

    【解决方案2】:

    您的代码对我来说看起来不错,而且我没有看到任何强制收集的单一原因。如果您的自定义类包含对 XDocument 中的 XElements 的引用,那么 GC 既不会收集它们也不会收集文档本身。如果其他东西持有对您的枚举的引用,那么它们也不会被收集。所以我真的很想看看你的自定义类定义以及它是如何填充的。

    【讨论】:

    • 添加了一个如何提取数据的示例,有一个 customclass 可以提取一个 xelement(或者它最近是否已更改),在 ExtractDetails 方法中它类似于 CustomClassX .XElementVariable = itemElement.Element(ElementName),这会不会一直保留引用并导致问题?
    • 如果您确实在 CurtomClassX 中的某处有 CustomClassX.XElementVariable = itemElement.Element(ElementName) 行,那么是的,它会创建对属于 XDocument 的 XElement 的引用。如果需要,您可能必须复制它。
    • 虽然我仍然对您通过简单地解析 XML 来运行内存这一事实感到困惑。您可能需要/必须考虑重组您的应用,使其不会读取太多数据。
    • 好吧,在切换代码之后,它似乎确实可以更好地管理内存,所以我认为这可能是问题所在,所以我将其标记为答案,很抱歉我不能给出赏金到期时的想法
    • 不用担心 :) 很高兴能提供帮助。
    【解决方案3】:

    您致电GC.Collect 的倾向是正确的。需要调用此方法表明您的代码有其他问题。

    但是,关于你的陈述,有几件事让我觉得你对记忆的理解有点不对劲。任务管理器不能很好地指示程序实际使用了多少内存;探查器对于这项任务要好得多。就内存而言,如果可以收集,GC 会在需要时收集内存。

    虽然它是一个措辞细节,但您会问如何“确保正在处理 XDocument 内存”。 Disposed 通常用于指手动释放非托管资源,例如数据库连接或文件句柄; GC 收集内存。

    现在尝试回答实际问题。引用您不释放的对象非常容易,尤其是在使用 lambda 和 LINQ 时。键入为IEnumerable 的内容特别容易出现这种情况,因为延迟评估的 LINQ 函数几乎总是会引入对您认为未使用的对象的引用。您省略的ReadXMLDocument 代码可能是开始查找的好地方。

    另一种可能性与 TomTom 的建议类似,即您使用的数据库类可能会存储您出于自身原因而没有预料到的对象。

    【讨论】:

      【解决方案4】:

      如果处理的 XML 文件太大(大约 500-800M),那么您就不能使用 XDocument(或 XmlDocument),因为它会尝试将整个文档加载到内存中。请参阅此讨论:Does LINQ handle large XML files? Getting OutOfMemoryException

      在这种情况下,您应该使用 XStreamingElement Class 并从中构建您的 ContainerClass。

      也许使用 64 位进程会有所帮助,但最佳做法是始终使用端到端的流式传输。

      【讨论】:

      • 目前我在 64 位处理器上运行它,虽然我同意使用 XDocument 加载大文件并不理想,但我的大多数文件都小得多,并且程序能够处理所有文件(它有多次,成功运行每个文档)只有当文档没有正确卸载时才会出现问题
      • @Manatherin - 即使只有一些文件很大,也足以杀死内存。 “未正确卸载”是什么意思?
      • 如 OP 中所述,我遇到的问题是有时 XDocument 使用的内存在 Document(以及其中的 Enumerables)超出范围时没有被释放,导致内存不足异常
      • @Manatherin - 当然,你不应该在 ContainerClass 实例中保留对 XDocument/XNode 等的任何引用...一旦你使用了它们,以确保它们是ReadXMLDocument 完成时收集。但是,如果您这样做了,那么您只会遇到“正常”的 OutOfMemory 问题,因为这些类 XDocument/XElement 等对 IDisposable 资源没有任何保留。并不是因为 IEnumerable 实现了 IDisposable 就意味着它背后确实有一些不受管理的东西。
      【解决方案5】:

      这不是真正的答案,更多的调查建议:如果 GC.Collect 没有帮助,那意味着你仍然在某处保留对对象的引用。 寻找可能保留引用的单例和缓存。

      如果你确实得到了异常或者可以收集内存转储,你可以使用 WinDbg+Sos 来查找谁持有对象的引用:搜索“内存泄漏 sos windbg”以查找详细信息。

      【讨论】:

        【解决方案6】:

        不管怎样,使用

        String.Equals(fileToRead.Extension, ".gz", StringComparison.OrdinalIgnoreCase)
        

        改为

        String.Compare()
        

        【讨论】:

          【解决方案7】:

          您可以尝试使用 tolist 强制评估:

          public static ContainerClass ReadXMLDocument(FileInfo fileToRead) 
          {     
            XDocument document = GetXDocument(fileToRead);     
            var containerClass = new ContainerClass();      
            //ForEach customClass in containerClass     
            //Read all data for customClass from XDocument      
            containerClass.CustomClassOne = document.Descendants(ElementName)
              .DescendantsAndSelf(ElementChildName)         
              .Select(a => ExtractDetails(a)).ToList();      
            return containerClass; 
          } 
          

          【讨论】:

            【解决方案8】:

            然后程序从 容器类到数据库。 导出容器类后 超出范围,但是, 垃圾收集器没有清理 容器类,因为它 存储为 IEnumerable,似乎领先 到 XDocument 留在内存中 (不确定这是否是原因,但 任务管理器显示内存 从 XDocument 没有被释放)。

            原因是 LYNC 将每个读取的项目都存储在它自己的事务参考池中。基本上,它这样做是为了在重读时可以使该项目独一无二。

            建议:

            • 仅将主键加载到数组中。提交。

            • 遍历列表并一次处理一个项目,在每个项目之后提交。

            【讨论】:

            • 我的代码中遇到了与 Manatherin 完全相同的问题,但我不明白您的答案,您能详细说明一下吗?
            • -1:我没听懂你说的一切,但我理解的部分不正确。
            • LINQ to SQL != LINQ to Objects != LINQ to XML。通用语法不需要通用实现。
            猜你喜欢
            • 1970-01-01
            • 2012-12-15
            • 2013-06-22
            • 2013-12-09
            • 1970-01-01
            • 2014-03-11
            • 1970-01-01
            • 2016-02-22
            • 1970-01-01
            相关资源
            最近更新 更多