System.OutOfMemoryException .NET 4.8 WPF 应用

System.OutOfMemoryException .NET 4.8 WPF App

提问人:Kyle 提问时间:11/8/2023 更新时间:11/11/2023 访问量:95

问:

我正在开发一个 WPF(WCF 体系结构)应用程序。这个应用程序有 2 个主要解决方案,客户端(前端)和控制器(后端),客户端与后端通信的方式是通过反射调用,本质上它构建了一个 XML 文档,其中包含要执行的方法、方法所在的命名空间和方法参数等信息......

所以这个具体的方法是上传文件。在文件大小> 1GB 之前,它可以完美运行。当客户端为反射调用生成 XML 文档时,会发生此错误。

我们像这样添加到 XML 文档中:

result.Add(new XElement(itemProperty.Name, new XCData(data)));

在本例中,itemProperty.Name 是方法名称,XCData 是方法参数。 因此,要上传的文档将是参数,我们以字节[]的形式接收它。我们需要将它作为字符串传递给 XCData 构造函数,因此使用以下内容:

string data= Convert.ToBase64String((byte[])value)

请注意,这适用于较小的文件,但对于 1GB 文件,在尝试将 byte[] 转换为 base64String 时会引发“System.OutOfMemoryException”异常。

我尝试以块形式读取数组,但是在调用 stringBuilder.ToString() 时,会抛出相同的异常......

 public static string HandleBigByteArray(byte[] arr)
        {
            try
            {
                
                #region approach 2
                int chunkSize = 104857600;//100MB
                StringBuilder b64StringBuilder = new StringBuilder();
                int offset = 0;

                while (offset < arr.Length)
                {
                    int remainingBytes = arr.Length - offset;
                    int bytesToEncode = Math.Min(chunkSize, remainingBytes);
                    string base64Chunk = Convert.ToBase64String(arr, offset, bytesToEncode);
                    b64StringBuilder.Append(base64Chunk);
                    offset += bytesToEncode;
                }


                return b64StringBuilder.ToString();
                #endregion
            }
            catch (Exception)
            {
                throw;
            }
        }

我不知道该怎么做或如何进一步调试/处理这个问题。

C# .NET WPF WCF 反射

评论

0赞 jdweng 11/8/2023
在 Net 中解析大型 XML 文件的唯一方法是使用 XmlReader。在以下帖子中查看我的解决方案:stackoverflow.com/questions/61607180/...
0赞 dbc 11/8/2023
XmlSerializer(和 LINQ to XML)如果文本值超过平台上的有效最大字符串长度,则将引发内存不足异常。当这种情况发生时,我同意@jdweng您需要使用 和 在某个临时文件(或 RecyclableMemoryStream)中写入和读取 XML。XmlReaderXmlWriter
0赞 dbc 11/8/2023
若要以增量方式读取巨大的 Base94 编码的 XML 文本值,请参阅 XmlReader - 如何在没有 System.OutOfMemoryException 的情况下读取元素中的超长字符串。在块中写入 Base64 时,可以为每个块重复调用 XmlWriter.WriteBase64()。
0赞 mm8 11/8/2023
@Kyle:使用这个班级对你有用吗?:stackoverflow.com/questions/2525533/......CryptoStream
0赞 Kyle 11/9/2023
大家好,感谢您的链接,我将检查所有内容,但解析 XML 不是问题。不确定它是否会成为一个。但是当字节数组调用 时会抛出异常。ToBase64String(),字节 = 1GB,一个字符串肯定可以容纳更多..?

答:

2赞 dbc 11/10/2023 #1

这里的基本问题是,在构造 Base64 字符串时,当您这样做时,您试图超过系统上可能的最大 .NET 字符串长度,正如 HitScan 在此回答中解释的那样,.NET 字符串的最大可能长度是多少?,在 64 位系统上最多包含字符。b64StringBuilder.ToString()int.MaxValue / 2

为了细分,您可以在 .NET Framework 上分配的最大连续内存块是字节,即 2GB。A 需要 2 个字节,而 Base64 编码将字符数夸大了 33%,因此您可以编码的最大字节数组大小约为 3/4 GB——这正是您所看到的。int.MaxValuechar

(请注意,如果设置了 gcAllowVeryLargeObjects,您将能够在内存中分配最多 4GB 的数组,因此如果这样做,请将上述计算乘以 2 倍。

为了解决这个特定问题,您可以创建一个包含有界大小的部分块的序列,并将它们全部添加到元素中,而不是创建一个包含数组的整个 Base64 内容的单个数组。当写入 XML 时,它们将被格式化为单个连续的文本值。XCDatabyte []XTextitemProperty.Name

为此,首先介绍以下扩展方法:

public static partial class XmlExtensions
{
    const int DefaultChunkLength = 8000;
    const int Base64BytesPerChunk = 3; // Base64 encodes 3 bytes to 4 characters.
    
    public static IEnumerable<XText> ToBase64XTextChunks(this Stream stream, int chunkLength = DefaultChunkLength)
    {
        return stream.ToBase64StringChunks(chunkLength).Select(s => new XText(s));
    }

    public static IEnumerable<XText> ToBase64XTextChunks(this IEnumerable<byte []> chunks, int chunkLength = DefaultChunkLength)
    {
        return chunks.Select(b => new ArraySegment<byte>(b)).ToBase64XTextChunks(chunkLength);
    }

    // In .NET Core I would use Memory<T> and/or ReadOnlyMemory<T> instead of ArraySegment<T>.

    public static IEnumerable<XText> ToBase64XTextChunks(this IEnumerable<ArraySegment<byte>> chunks, int chunkLength = DefaultChunkLength)
    {
        return chunks.ToBase64StringChunks(chunkLength).Select(s => new XText(s));
    }

    internal static IEnumerable<string> ToBase64StringChunks(this Stream stream, int chunkLength = DefaultChunkLength)
    {
        if (stream == null)
            throw new ArgumentNullException("stream");
        if (chunkLength < 1 || chunkLength > int.MaxValue / Base64BytesPerChunk)
            throw new ArgumentOutOfRangeException("chunkLength < 1 || chunkLength > int.MaxValue / Base64BytesPerChunk");
        var buffer = new byte[Math.Max(300, Base64BytesPerChunk * DefaultChunkLength)];
        return ToBase64StringChunksEnumerator(stream.ReadAllByteChunks(buffer), chunkLength);
    }
    
    internal static IEnumerable<string> ToBase64StringChunks(this IEnumerable<ArraySegment<byte>> chunks, int chunkLength = DefaultChunkLength)
    {
        if (chunks == null)
            throw new ArgumentNullException("chunks");
        if (chunkLength < 1 || chunkLength > int.MaxValue / 3)
            throw new ArgumentOutOfRangeException("chunkLength < 1 || chunkLength > int.MaxValue / 3");
        return ToBase64StringChunksEnumerator(chunks, chunkLength);
    }
    
    static IEnumerable<string> ToBase64StringChunksEnumerator(this IEnumerable<ArraySegment<byte>> chunks, int chunkLength)
    {
        var buffer = new byte[Base64BytesPerChunk*chunkLength];
        foreach (var chunk in chunks.ToFixedSizedChunks(buffer))
        {
            yield return Convert.ToBase64String(chunk.Array, chunk.Offset, chunk.Count);
        }
    }
    
    internal static IEnumerable<ArraySegment<byte>> ReadAllByteChunks(this Stream stream, byte [] buffer)
    {
        if (stream == null)
            throw new ArgumentNullException("stream");
        if (buffer == null)
            throw new ArgumentNullException("buffer");
        if (buffer.Length < 1)
            throw new ArgumentException("buffer.Length < 1");
        return ReadAllByteChunksEnumerator(stream, buffer);
    }

    static IEnumerable<ArraySegment<byte>> ReadAllByteChunksEnumerator(Stream stream, byte [] buffer)
    {
        int nRead;
        while ((nRead = stream.Read(buffer, 0, buffer.Length)) > 0)
            yield return new ArraySegment<byte>(buffer, 0, nRead);
    }
}

public static partial class EnumerableExtensions
{
    public static IEnumerable<ArraySegment<T>> ToFixedSizedChunks<T>(this IEnumerable<ArraySegment<T>> chunks, T [] buffer)
    {
        if (chunks == null)
            throw new ArgumentNullException("chunks");
        if (buffer.Length == 0)
            throw new ArgumentException("buffer.Length == 0");
        return ToFixedSizedChunksEnumerator(chunks, buffer);
    }
    
    static IEnumerable<ArraySegment<T>> ToFixedSizedChunksEnumerator<T>(IEnumerable<ArraySegment<T>> chunks, T [] buffer) 
    {
        int bufferIndex = 0;
        bool anyRead = false, anyReturned = false;
        foreach (var chunk in chunks)
        {
            anyRead = true;
            int chunkIndex = 0;
            while (chunkIndex < chunk.Count)
            {
                int toCopy = Math.Min(buffer.Length - bufferIndex, chunk.Count - chunkIndex);
                if (toCopy > 0)
                {
                    chunk.CopyTo(chunkIndex, buffer, bufferIndex, toCopy);
                    bufferIndex += toCopy;
                    if (bufferIndex == buffer.Length)
                    {
                        yield return new ArraySegment<T>(buffer, 0, bufferIndex);
                        bufferIndex = 0;
                        anyReturned = true;
                    }
                }
                chunkIndex += toCopy;
            }
        }
        // If passed an enumerable of empty chunks we should still return one empty chunk.  But if there were no chunks at all, return nothing.
        if (bufferIndex > 0 || (anyRead && !anyReturned))
            yield return new ArraySegment<T>(buffer, 0, bufferIndex);
    }
    
    public static void CopyTo<T>(this ArraySegment<T> from, int fromIndex, T [] destination, int destinationIndex, int count)
    {
        Buffer.BlockCopy(from.Array, checked(from.Offset + fromIndex), destination, destinationIndex, count);
    }
}

现在,假设您的值实际上是从文件中读取的,您可以执行以下操作:byte[] arrfileName

var result = new XElement("root");

var e = new XElement(itemProperty.Name);
using (var stream = File.OpenRead(fileName))
{
    foreach (var text in stream.ToBase64XTextChunks())
        e.Add(text);
}
result.Add(e);

扩展方法以块的形式读取流,并以块的形式进行编码,因此您永远不会达到最大数组大小或最大字符串长度。stream.ToBase64XTextChunks()

但是,如果你已经在内存中拥有巨大的数组,你可以这样做:byte [] arr

    foreach (var text in new [] { arr }.ToBase64XTextChunks())
        e.Add(text);

笔记

  • 出于性能原因,我建议将缓冲区和字符串分配保持在 80,000 字节以下,以便它们不会进入大型对象堆

  • 在 .NET Core 中,我会使用 and 而不是旧的 .Memory<T>ReadOnlyMemory<T>ArraySegment<T>

在这里演示小提琴。

话虽如此,您也非常接近达到其他内存限制,包括:

  • 最大数组大小为 ,即 2 GB。byte []int.MaxValue

    设置不会增加此限制。 仅增加数组可以容纳的内存量。长度仍限制为 。gcAllowVeryLargeObjectsgcAllowVeryLargeObjectsint.MaxValue

    此限制在客户端和服务器端都将是一个问题。

  • 可以由 保存的最大字符数,从引用源可以看出是 。StringBuilderint.MaxValue

  • 服务器的总可用虚拟内存。您当前的设计似乎根本没有限制上传大小。这似乎使您的服务器容易受到拒绝服务攻击,攻击者会继续上传数据,直到您的服务器内存不足。

    如果您有大量客户端同时上传大量数据,即使没有一个客户端尝试进行 DOS 攻击,您的服务器也将再次耗尽内存。

  • 客户端的可用虚拟内存。如果客户端在资源受限的环境(如智能手机)中运行,则可能无法分配大量内存。

我建议你重新考虑你的设计,允许任意大的文件上传,并在客户端和服务器端的内存中缓冲它们。即使出于业务原因,您决定需要支持上传大于 1 或 2 GB 的文件,我也建议您采用流式处理解决方案,即使用和读取内容,而无需将整个内容加载到内存中。要开始使用,请参阅XmlWriterXmlReader