在使用xmlserializer时出现了类型为“System.OutOfMemoryException”的异常。

3
我正在使用以下代码获取XML字符串。
public static string ToXMLString(object obj, string nodeName)
{
    XmlSerializer xmlSerializer = default(XmlSerializer);
    string xml = string.Empty;
    StreamReader r = default(StreamReader);
    try
    {
        if (obj != null)
        {
            using (MemoryStream m = new MemoryStream())
            {
                using (XmlWriter writer = XmlWriter.Create(m, new XmlWriterSettings() { OmitXmlDeclaration = true, Indent = true }))
                {
                    // Don't include XML namespace
                    XmlSerializerNamespaces xmlnsEmpty = new XmlSerializerNamespaces();
                    xmlnsEmpty.Add("", "");
                    if (xmlSerializer == null)
                        xmlSerializer = new XmlSerializer(obj.GetType(), new XmlRootAttribute(nodeName));
                    xmlSerializer.Serialize(writer, obj, xmlnsEmpty);

                    m.Flush();
                    m.Position = 0;

                    r = new StreamReader(m);
                    xml = r.ReadToEnd();
                    xmlSerializer = null;
                }
            }
        }

        return xml;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }
    finally
    {
        r.Close();
        r.Dispose();
    }
    //XmlSerializer xmlSerializer;

}

我有一个使用方法运行的循环,在一段时间后,我会得到以下内存溢出异常:

这个异常的原因是什么?使用语句确实释放了流吗?还是我可以使用其他替代方案?


它不会影响代码,但在顶部声明那些变量或将xmlSerializer赋值为null没有任何好处;我只是在行内声明,即var xmlSerializer = new XmlSerializer(...);。你没有处理读取器的释放,但这里不会对你造成影响。实际上,如果你只想要字符串,你可以写入到StringBuilder中 - 避免与Stream打交道... - Marc Gravell
当使用 using 块退出时,会调用 IDisposable.Dispose() 方法。因此,您代码的这一部分是正确的。 - C.Evenhuis
我们能看到你正在序列化的对象模型吗?我怀疑这与模型有关 - 类似于循环引用(逃避内置检测的循环引用 - 不是不可能做到),或类似的问题。 - Marc Gravell
1
这与OOM错误无关,但是这是同一段代码的稍微重构和更直接的版本:http://pastie.org/3248365 - Marc Gravell
你想知道为什么这里要两次使用 default(StreamReader) 语法吗?此外,你正在覆盖 r 而没有处理先前的实例。它将会超出范围并被垃圾收集,但不如显式处理释放的那么快。 - Kevin P. Rice
@Kevin 这与此无关 - 它只是表示“null”; 不太清楚,但也不是问题。只有一个读取器(它没有被覆盖),尽管它也没有被处理 - 但是...再次强调,这不是问题。 - Marc Gravell
2个回答

10

我认为这里的问题是程序集饱和。 XmlSerializer 的工作原理是动态生成一个程序集;如果你使用 XmlSerializer(Type) 构造函数,它会缓存并查找;但对于任何其他构造函数,它不会进行缓存。而程序集通常无法卸载。因此,你只会得到越来越多的程序集占用内存。如果你在循环中运行该代码,则需要缓存序列化程序:

using System;
using System.Collections;
using System.IO;
using System.Xml;
using System.Xml.Serialization;


public static class Program
{
    static void Main()
    {
        // the loop here is from your comment
        for (int i = 0; i < 10000000; i++) { ToXMLString("test", string.Format("test")); Console.WriteLine(i); }
    }

    // why is this Hashtable? due to the threading semantics!
    private static readonly Hashtable serializerCache = new Hashtable();

    public static string ToXMLString(object obj, string nodeName)
    {
        if (obj == null) throw new ArgumentNullException("obj");
        Type type = obj.GetType();
        var cacheKey = new { Type = type, Name = nodeName };
        XmlSerializer xmlSerializer = (XmlSerializer)serializerCache[cacheKey];
        if (xmlSerializer == null)
        {
            lock (serializerCache)
            { // double-checked
                xmlSerializer = (XmlSerializer)serializerCache[cacheKey];
                if (xmlSerializer == null)
                {
                    xmlSerializer = new XmlSerializer(type, new XmlRootAttribute(nodeName));
                    serializerCache.Add(cacheKey, xmlSerializer);
                }
            }
        }
        try
        {

            StringWriter sw = new StringWriter();
            using (XmlWriter writer = XmlWriter.Create(sw,
                new XmlWriterSettings() { OmitXmlDeclaration = true, Indent = true }))
            {
                // Don't include XML namespace
                XmlSerializerNamespaces xmlnsEmpty = new XmlSerializerNamespaces();
                xmlnsEmpty.Add("", "");
                xmlSerializer.Serialize(writer, obj, xmlnsEmpty);
            }
            return sw.ToString();
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine(ex.Message);
            throw;
        }
    }
}

for (int i = 0; i < 10000000; i++) { t.ToXMLString("test", string.Format("test"));Console.WriteLine(i); GC.Collect();} - user1010863
@user1010863 我不知道如何解释那个评论... - 还有,string.Format("test") - 为什么要这样做? - Marc Gravell
@user1010863 等一下...是 "devenv" 在增长吗?还是实际的exe在增长?如果是 "devenv",可能只是IDE - 也许是Intellitrace。尝试在调试器外运行它(Ctrl + F5)。我已经将其与我的代码一起运行(如上所述)循环,并且内存非常稳定;没有增长。 - Marc Gravell
@user1010863 了解一下,如果没有缓存,做同样的操作会:a.非常非常慢,b.耗费更多内存(即使迭代次数只有几百次,比起启用汇编语言缓存的代码在500k次迭代时使用的内存还要多,并且不断增长)。启用汇编语言缓存后,在4,350,000次迭代时,我的工作集为2644k,保持稳定。 - Marc Gravell
非常感谢你提供的样例。我现在正在测试它,看起来运行良好。 - user1010863
显示剩余4条评论

5
这里的问题可能不是这段代码本身,而是你在该方法之外对生成的字符串所做的操作。根据你序列化的内容,很可能会产生许多大型字符串。如果你在循环中持续保留这些字符串,就会消耗越来越多的内存。更糟糕的是,即使使用的内存绝对量可能并不巨大,这些大型字符串很可能会导致内存碎片化 - 垃圾回收器可能无法为下一个字符串分配连续的内存块。在CLR中,大对象(大约85KB)不会被分配到通常的垃圾回收代中;相反,它们进入大对象堆。除非在.Net 4中发生了变化,否则该堆永远不会被压缩。由此产生的影响是,如果你分配并持有大量字符串,就会出现越来越少的连续空闲块,这些块足够大以分配下一个字符串。这是因为没有将已分配的块在其他内存块释放时压缩在一起的过程。这种情况很容易导致内存不足异常,正如上面所描述的,在进行这种操作时。

这篇文章很好地概述了大对象堆的“危险”和注意事项。

您使用此方法返回的字符串做什么,生成的字符串有多大?


据我所了解,.Net 4.5(及更高版本)现在会对大对象堆进行压缩。 - musefan

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