提问人:Kyle 提问时间:11/8/2023 更新时间:11/11/2023 访问量:95
System.OutOfMemoryException .NET 4.8 WPF 应用
System.OutOfMemoryException .NET 4.8 WPF App
问:
我正在开发一个 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;
}
}
我不知道该怎么做或如何进一步调试/处理这个问题。
答:
这里的基本问题是,在构造 Base64 字符串时,当您这样做时,您试图超过系统上可能的最大 .NET 字符串长度,正如 HitScan 在此回答中解释的那样,.NET 字符串的最大可能长度是多少?,在 64 位系统上最多包含字符。b64StringBuilder.ToString()
int.MaxValue / 2
为了细分,您可以在 .NET Framework 上分配的最大连续内存块是字节,即 2GB。A 需要 2 个字节,而 Base64 编码将字符数夸大了 33%,因此您可以编码的最大字节数组大小约为 3/4 GB——这正是您所看到的。int.MaxValue
char
(请注意,如果设置了 gcAllowVeryLargeObjects
,您将能够在内存中分配最多 4GB 的数组,因此如果这样做,请将上述计算乘以 2 倍。
为了解决这个特定问题,您可以创建一个包含有界大小的部分块的序列,并将它们全部添加到元素中,而不是创建一个包含数组的整个 Base64 内容的单个数组。当写入 XML 时,它们将被格式化为单个连续的文本值。XCData
byte []
XText
itemProperty.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[] arr
fileName
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
设置不会增加此限制。 仅增加数组可以容纳的内存量。长度仍限制为 。
gcAllowVeryLargeObjects
gcAllowVeryLargeObjects
int.MaxValue
此限制在客户端和服务器端都将是一个问题。
可以由 保存的最大字符数,从引用源可以看出是 。
StringBuilder
int.MaxValue
服务器的总可用虚拟内存。您当前的设计似乎根本没有限制上传大小。这似乎使您的服务器容易受到拒绝服务攻击,攻击者会继续上传数据,直到您的服务器内存不足。
如果您有大量客户端同时上传大量数据,即使没有一个客户端尝试进行 DOS 攻击,您的服务器也将再次耗尽内存。
客户端的可用虚拟内存。如果客户端在资源受限的环境(如智能手机)中运行,则可能无法分配大量内存。
我建议你重新考虑你的设计,允许任意大的文件上传,并在客户端和服务器端的内存中缓冲它们。即使出于业务原因,您决定需要支持上传大于 1 或 2 GB 的文件,我也建议您采用流式处理解决方案,即使用和读取内容,而无需将整个内容加载到内存中。要开始使用,请参阅XmlWriter
XmlReader
评论
XmlSerializer
(和 LINQ to XML)如果文本值超过平台上的有效最大字符串长度,则将引发内存不足异常。当这种情况发生时,我同意@jdweng您需要使用 和 在某个临时文件(或RecyclableMemoryStream
)中写入和读取 XML。XmlReader
XmlWriter
XmlWriter.WriteBase64()。
CryptoStream