创建多个字节数组时出现OutOfMemoryException异常

6
我经常在创建和处理一些字节数组的方法中遇到 OutOfMemoryException。代码如下:
  1. 创建一个内存流以获取一些数据(大约60MB)。
  2. 创建与内存流相同大小的字节数组(大约60MB)
  3. 用来自内存流的字节填充数组
  4. 关闭内存流
  5. 处理来自字节数组的数据
  6. 离开该方法
当我调用这个方法20-30次时,我会在分配字节数组的地方遇到 OutOfMemoryException。但我不认为这是系统内存问题。应用程序内存使用量约为500MB(私有工作集),测试机是64位,RAM为4GB。
可能是字节数组或 MemoryStream 使用的内存没有在方法结束后释放吗?但是,看起来这个内存并没有为进程分配,因为私有工作集只有500MB左右。
除了物理内存短缺之外,什么可能导致创建大字节数组(60MB)时出现 OutOfMemoryException[编辑以添加代码示例] 源代码来自PdfSharp lib 异常抛出在这一行:byte[] imageBits = new byte[streamLength]; 确实看起来像是 LOH 碎片问题。
/// <summary>
/// Reads images that are returned from GDI+ without color palette.
/// </summary>
/// <param name="components">4 (32bpp RGB), 3 (24bpp RGB, 32bpp ARGB)</param>
/// <param name="bits">8</param>
/// <param name="hasAlpha">true (ARGB), false (RGB)</param>
private void ReadTrueColorMemoryBitmap(int components, int bits, bool hasAlpha)
{
  int pdfVersion = Owner.Version;
  MemoryStream memory = new MemoryStream();
  image.gdiImage.Save(memory, ImageFormat.Bmp);
  int streamLength = (int)memory.Length;

  if (streamLength > 0)
  {
    byte[] imageBits = new byte[streamLength];
    memory.Seek(0, SeekOrigin.Begin);
    memory.Read(imageBits, 0, streamLength);
    memory.Close();

    int height = image.PixelHeight;
    int width = image.PixelWidth;

    if (ReadWord(imageBits, 0) != 0x4d42 || // "BM"
        ReadDWord(imageBits, 2) != streamLength ||
        ReadDWord(imageBits, 14) != 40 || // sizeof BITMAPINFOHEADER
        ReadDWord(imageBits, 18) != width ||
        ReadDWord(imageBits, 22) != height)
    {
      throw new NotImplementedException("ReadTrueColorMemoryBitmap: unsupported format");
    }
    if (ReadWord(imageBits, 26) != 1 ||
      (!hasAlpha && ReadWord(imageBits, 28) != components * bits ||
       hasAlpha && ReadWord(imageBits, 28) != (components + 1) * bits) ||
      ReadDWord(imageBits, 30) != 0)
    {
      throw new NotImplementedException("ReadTrueColorMemoryBitmap: unsupported format #2");
    }

    int nFileOffset = ReadDWord(imageBits, 10);
    int logicalComponents = components;
    if (components == 4)
      logicalComponents = 3;

    byte[] imageData = new byte[components * width * height];

    bool hasMask = false;
    bool hasAlphaMask = false;
    byte[] alphaMask = hasAlpha ? new byte[width * height] : null;
    MonochromeMask mask = hasAlpha ?
      new MonochromeMask(width, height) : null;

    int nOffsetRead = 0;
    if (logicalComponents == 3)
    {
      for (int y = 0; y < height; ++y)
      {
        int nOffsetWrite = 3 * (height - 1 - y) * width;
        int nOffsetWriteAlpha = 0;
        if (hasAlpha)
        {
          mask.StartLine(y);
          nOffsetWriteAlpha = (height - 1 - y) * width;
        }

        for (int x = 0; x < width; ++x)
        {
          imageData[nOffsetWrite] = imageBits[nFileOffset + nOffsetRead + 2];
          imageData[nOffsetWrite + 1] = imageBits[nFileOffset + nOffsetRead + 1];
          imageData[nOffsetWrite + 2] = imageBits[nFileOffset + nOffsetRead];
          if (hasAlpha)
          {
            mask.AddPel(imageBits[nFileOffset + nOffsetRead + 3]);
            alphaMask[nOffsetWriteAlpha] = imageBits[nFileOffset + nOffsetRead + 3];
            if (!hasMask || !hasAlphaMask)
            {
              if (imageBits[nFileOffset + nOffsetRead + 3] != 255)
              {
                hasMask = true;
                if (imageBits[nFileOffset + nOffsetRead + 3] != 0)
                  hasAlphaMask = true;
              }
            }
            ++nOffsetWriteAlpha;
          }
          nOffsetRead += hasAlpha ? 4 : components;
          nOffsetWrite += 3;
        }
        nOffsetRead = 4 * ((nOffsetRead + 3) / 4); // Align to 32 bit boundary
      }
    }
    else if (components == 1)
    {
      // Grayscale
      throw new NotImplementedException("Image format not supported (grayscales).");
    }

    FlateDecode fd = new FlateDecode();
    if (hasMask)
    {
      // monochrome mask is either sufficient or
      // provided for compatibility with older reader versions
      byte[] maskDataCompressed = fd.Encode(mask.MaskData);
      PdfDictionary pdfMask = new PdfDictionary(document);
      pdfMask.Elements.SetName(Keys.Type, "/XObject");
      pdfMask.Elements.SetName(Keys.Subtype, "/Image");

      Owner.irefTable.Add(pdfMask);
      pdfMask.Stream = new PdfStream(maskDataCompressed, pdfMask);
      pdfMask.Elements[Keys.Length] = new PdfInteger(maskDataCompressed.Length);
      pdfMask.Elements[Keys.Filter] = new PdfName("/FlateDecode");
      pdfMask.Elements[Keys.Width] = new PdfInteger(width);
      pdfMask.Elements[Keys.Height] = new PdfInteger(height);
      pdfMask.Elements[Keys.BitsPerComponent] = new PdfInteger(1);
      pdfMask.Elements[Keys.ImageMask] = new PdfBoolean(true);
      Elements[Keys.Mask] = pdfMask.Reference;
    }
    if (hasMask && hasAlphaMask && pdfVersion >= 14)
    {
      // The image provides an alpha mask (requires Arcrobat 5.0 or higher)
      byte[] alphaMaskCompressed = fd.Encode(alphaMask);
      PdfDictionary smask = new PdfDictionary(document);
      smask.Elements.SetName(Keys.Type, "/XObject");
      smask.Elements.SetName(Keys.Subtype, "/Image");

      Owner.irefTable.Add(smask);
      smask.Stream = new PdfStream(alphaMaskCompressed, smask);
      smask.Elements[Keys.Length] = new PdfInteger(alphaMaskCompressed.Length);
      smask.Elements[Keys.Filter] = new PdfName("/FlateDecode");
      smask.Elements[Keys.Width] = new PdfInteger(width);
      smask.Elements[Keys.Height] = new PdfInteger(height);
      smask.Elements[Keys.BitsPerComponent] = new PdfInteger(8);
      smask.Elements[Keys.ColorSpace] = new PdfName("/DeviceGray");
      Elements[Keys.SMask] = smask.Reference;
    }

    byte[] imageDataCompressed = fd.Encode(imageData);

    Stream = new PdfStream(imageDataCompressed, this);
    Elements[Keys.Length] = new PdfInteger(imageDataCompressed.Length);
    Elements[Keys.Filter] = new PdfName("/FlateDecode");
    Elements[Keys.Width] = new PdfInteger(width);
    Elements[Keys.Height] = new PdfInteger(height);
    Elements[Keys.BitsPerComponent] = new PdfInteger(8);
    // TODO: CMYK
    Elements[Keys.ColorSpace] = new PdfName("/DeviceRGB");
    if (image.Interpolate)
      Elements[Keys.Interpolate] = PdfBoolean.True;
  }
}

3
第一个问题,你是否正确地处理了流?此外,你可能需要提供一些代码。 - CodingGorilla
@CodingGorilla 除了 MemoryStream 外,没有需要处理的内容。它已经被关闭了。而且事实上,MemoryStream.Close() 只是调用了 MemoryStream.Dispose(true),然后是 GC.SuppressFinalize(this)。字节数组不可处理,所以我无法再做更多的处理。至于代码……它相当复杂,来自 PdfSharp 库。这不是我的代码,但我正在尝试理解和修复它所呈现的问题。 - SiliconMind
我正在研究一些分段问题。似乎在64位Windows上,底层内存管理器不应该暴露这种行为。你的进程是否可能在WOW64中运行,也就是说,你只编译了x86版本?我更新了我的答案,提供了更多关于分段问题的信息。 - Abel
这里有相当多的副本和辅助数组在飞来飞去。这可能是一个罕见的情况,调用GC.Collect()可能会有所帮助,最好是在ReadTrueColorMemoryBitmap()之后或结束时进行。 - H H
我们都认为问题出在GC无法恢复或碎片化托管内存堆上。那么,一个好的替代方案可能是使用非托管内存:System.IO.UnmanagedMemoryStream。问题是你必须事先知道所需的空间。或者至少有一个上限。文档明确表示此流不会在堆上分配内存。另一个问题是您的程序需要安全设置以允许执行此操作。 - JotaBe
6个回答

6
我希望您正在使用 MemoryStream.GetBuffer(),而不是复制到新数组。
您主要的问题不是直接缺少内存,而是 LOH 的碎片化。这可能是一个棘手的问题,主要问题是分配大小不同的大缓冲区。在 LOH 上的项被收集但不被压缩。
解决方案可能是:
  • 首先确保您没有阻止任何内容被收集。使用分析器。
  • 尝试重用缓冲区。
  • 将分配舍入到一组固定数字上。
后两种方法都需要您使用过大的数组,并可能需要一些工作。

@SiliconMind:我能想到的一个方法是使用一个小的MemoryStream,只向目标字节数组写入小块数据。这样至少可以避免两个大数组之一存在,并且在LOH(大对象堆)碎片化方面可以节省很多空间。 - Abel
@HenkHolterman:我已经编辑了问题,包括代码示例。是的,我可以更改代码来解决这个问题。事实上,我更喜欢使用Bitmap.LockBits,但我真正想要的是你们的解释为什么 :) 我想理解并知道这些OutOfMemoryExceptions的真正原因。 - SiliconMind
1
接受这个答案,因为它似乎确实是LOH碎片问题。使用MemoryStream.GetBuffer()可以解决问题,但是使用Bitmap.LockBits可以获得更好的结果。 - SiliconMind
好的,很高兴你找到了一些解决方案。你试过使用GC.Collect()了吗? - H H
@HenkHolterman 在这种情况下,GC.Collect() 没有起到任何帮助作用。在处理每个 PDF 页面后,我仍然在自己的代码中使用它。总体上,它确实有所帮助,但导致 LOH 问题的代码仍然存在。然而,如果我不调用 GC.Collect(),可能在调用该方法 20-30 次后就会出现 OutOfMemoryException... 可能在第 10 次调用后就会出现该异常 :) - SiliconMind
显示剩余4条评论

3

建议处理MemoryStream,但这对你没有任何作用。也有人建议使用GC.Collect,但这可能不会有帮助,因为似乎你的内存并没有大量增长。调用GC.Collect需要小心,它可能是一项昂贵的操作。

碎片化

看起来你正在遇到臭名昭著的大对象堆碎片问题。这很可能是由于经常分配和释放60MB内存块导致的。如果LOH被碎片化,它将保持碎片化。这是长时间运行的.NET应用程序的主要问题,也是ASP.NET经常在间隔时间重启的原因之一。

避免OutOfMemoryException

请参考上面的CodeProject文章了解如何做到这一点。诀窍在于使用MemoryFailPoint并捕获InsufficientMemoryException。这样,您可以优雅地降低应用程序的负荷,使其不会变得不稳定。

可能的通用解决方案

确保您的大型对象尽可能长时间存活。重复使用缓冲区。使用足够的大小仅分配一次,并在需要再次使用缓冲区时将其清零。这样,您就不会遇到任何其他内存问题。当您的对象保持在85k以下时,它们通常不会进入LOH(大对象堆),也不会造成混乱。

64位机器不应该有这个问题

编辑:根据这篇帖子(解决方案选项卡)和这篇帖子(查看开放评论),这个问题不应该出现在64位机器上。既然你说你在64位机器上运行代码,也许你是使用x86配置编译的?


尽管我的测试机器是64位的,但目标平台不幸是32位的WinXP。看起来CodeProject上描述的问题已经很久以前被MS的补丁修复了。 - SiliconMind
@SiliconMind:不,问题还没有解决。只是在64位模式下,由于更大的虚拟地址空间(而不是物理地址空间),它会发生得更少。为什么要针对32位?如果你的测试机器是64位并且运行Windows 7,那么请为64位构建你的项目,并查看是否有所不同。 - Abel

0

你的堆内存抛出了这个异常,请在最后调用GC.Collect()来释放资源。你也可以使用MemoryProfiler来查找堆内存使用情况,它带有14天的试用期。


-1

尝试将其包含在using(MemoryStream x = ...) { }块中,这将为您处理对象的释放。

虽然Close应该Dispose对象,但根据.NET指南,也许在MemoryStream中有所不同。


在之前已被删除的非nb的回答中,曾经提到过这个方法。然而,正如Jon Skeet在那里评论的那样,释放MemoryStream并不会释放缓冲区(它只是将Open和Writable标志设置为false)。GC需要介入以释放字节数组。 - Abel
当然。你应该让垃圾回收器处理内存,或者不要过于频繁地使用 GC.Collect()。但是使用块可以保证即使出现问题(例如抛出异常或忘记关闭/释放对象),流也会被释放。 - JotaBe
我同意,但在MemoryStream的情况下,这并不是真的。 MS持有的唯一资源是一个字节数组,没有非托管资源。在对象是可处置的时使用using{..}是一个好习惯,但在某些情况下,对象仅因继承链需要而IDisposable。在这种情况下,Dispose()实际上是一个空操作,_不会释放资源_。如果您不相信我的说法,请查看Reflector。注意:在这里使用using不会有任何损害,但也不会做任何事情。 - Abel
谢谢您的评论。您说得对。在许多情况下这并不重要,但我相信您同意,它会有所帮助,因为当离开“using”代码块范围时,对象的引用就会消失,因此可以通过GC进行回收。这将发生在任何类型的作用域中(例如for循环体或其他语法结构)。如果我说错了,请纠正我。 - JotaBe

-2

我在下面的代码中有同样的问题:

ImageData = (byte[])pDataReader.qDataTable.Rows[0][11];
if (ImageData != null)
{
    ms = new MemoryStream(ImageData);
    button1.BackgroundImage = Image.FromStream(ms);
}
ImageData = null;

ImageData = (byte[])pDataReader.qDataTable.Rows[0][12];
if (ImageData != null)
{
    ms = new MemoryStream(ImageData);
    button1.BackgroundImage = Image.FromStream(ms);
}
ImageData = null;
ms.Close();

删除ms.Close();可以解决问题。
我认为问题出现是因为你在if块外定义了MemoryStream memory = new MemoryStream();并在if块中关闭它,就像我一样! 异常 调试信息


当关闭一个未被分配的流时,你真的会收到“内存不足”异常吗?问题中的代码总是在“if”之前创建并填充流,因此我看不出你“答案”与原始问题有任何关联。 - I liked the old Stack Overflow
@Alessio Cantarella;是的,我遇到了“内存不足”的异常,正如我所说,我不知道为什么,也许你可以尝试SiliconMind代码并回答这个问题。它被分配了,否则无法构建。 我编辑了我的答案,添加了两张照片,请查看。 - jaleel

-4
首先,不建议通过TSQL读写大型FILESTREAM数据。推荐的方法是使用SQL服务器提供的Win32/DOTNET API。我上面发布的代码展示了如何使用SqlFileStream() .net类访问FILESTREAM数据。它还展示了如何将数据分成较小的块发送。
只要您的总内存足够,您可以通过创建一堆较小的数组并将它们包装在单个IList或其他索引接口中来防止由LOH碎片化导致的内存不足异常。

4
这不是与SQL相关的问题,没有涉及到SQL。你在说什么? - SiliconMind

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