内存耗尽

5

我这里有一些代码

   private void Run()
    {
        MyClass c = new MyClass();
        c.Load(somepath1);
        using (StreamReader sr = new StreamReader(filepath))
        {
            string line = string.Empty;
            while ((line = sr.ReadLine()) != null)
            {
                using (Bitmap B = new Bitmap(line))
                {
                    Point p = SomeMethod(ref c, new Point());
                    using (MemoryStream ms = new MemoryStream())
                    {
                        B.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
                        using (Bitmap T = new Bitmap(new Bitmap(Image.FromStream(ms))))
                        using (Graphics g = Graphics.FromImage(T))
                        {
                            g.DrawEllipse(new Pen(Brushes.Red, 4), p.X - 5, p.Y - 5, 10, 10);
                            FileInfo fi = new FileInfo(somepath2);
                            T.Save(Path.Combine(somepath3, fi.Name));
                        }
                    }
                }
            }
        }
    } 

并且函数 SomeMethod 是:

    Point SomeMethod(ref MyClass c, Point mid)
    {
        float[] Mat = new float[9];
        Point p;

        c.Method1(Mat);
        c.Method2(Mat, out p);

        return p;
    }

MyClass 是:

public class MyClass
{
    public void Method1(float[] Mat, out Point point)
    {
        //calculation point from values in Mat
    }

    public void Method2(float[] Mat)
    {
        //Do some Operation in Mat
    }

    public void Load(string FileName)
    {
        //Do Some Data Loading From a small file about 400 byte
    }
}

StreamReader sr打开了一个包含大约400行图像位置的文件,我根据我的计算读取它们并在它们上面绘制一些东西,我没有使用任何外部库或不安全的代码。问题是为什么我会耗尽内存?

-------------编辑--------------------

程序启动时使用约20MB的内存,在调用Run之后,内存使用量开始增加,如果我运行大约200张图片,内存使用量就会达到约1.7GB,并且Run函数完成工作后,内存使用量会恢复到20MB。

------------编辑------------ 将位图B保存在MemoryStream中是因为图形无法使用索引像素格式图像。 主要问题是垃圾回收器在这里做了什么? 我没有留在内存中的对象。

----------编辑----------------

异常是:

System.OutOfMemoryException was unhandled
  Message=Out of memory.
  Source=System.Drawing
  StackTrace:
       at System.Drawing.Graphics.CheckErrorStatus(Int32 status)
       at System.Drawing.Graphics.DrawImage(Image image, Int32 x, Int32 y, Int32 width, Int32 height)
       at System.Drawing.Bitmap..ctor(Image original, Int32 width, Int32 height)
       at System.Drawing.Bitmap..ctor(Image original)
       at WindowsFormsApplication1.Form1.buttonrun1_Click(Object sender, EventArgs e) in C:\Users\hamidp\Desktop\WindowsFormsApplication1\WindowsFormsApplication1\Form1.cs:line 115
       at System.Windows.Forms.Control.OnClick(EventArgs e)
       at System.Windows.Forms.Button.OnClick(EventArgs e)
       at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
       at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
       at System.Windows.Forms.Control.WndProc(Message& m)
       at System.Windows.Forms.ButtonBase.WndProc(Message& m)
       at System.Windows.Forms.Button.WndProc(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
       at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
       at System.Windows.Forms.NativeWindow.DebuggableCallback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
       at System.Windows.Forms.UnsafeNativeMethods.DispatchMessageW(MSG& msg)
       at System.Windows.Forms.Application.ComponentManager.System.Windows.Forms.UnsafeNativeMethods.IMsoComponentManager.FPushMessageLoop(Int32 dwComponentID, Int32 reason, Int32 pvLoopData)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoopInner(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.ThreadContext.RunMessageLoop(Int32 reason, ApplicationContext context)
       at System.Windows.Forms.Application.Run(Form mainForm)
       at WindowsFormsApplication1.Program.Main() in C:\Users\hamidp\Desktop\WindowsFormsApplication1\WindowsFormsApplication1\Program.cs:line 17
       at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()
  InnerException: 

在第 using (Bitmap T = new Bitmap(new Bitmap(Image.FromStream(ms)))) 行抛出异常。

-------------------编辑-----------------------

我还在 while ((line = sr.ReadLine()) != null) 后面添加了一行 GC.Collect();,但是同样的错误再次发生。


5
你的图片尺寸是多少?你使用的是哪个操作系统(32位/64位)?你正在编译哪个平台? - Oded
Windows 7 专业版 64 位,图像大约为 350KB 的 jpeg 格式,使用 Microsoft Visual Studio 2010 和 WinForms。 - user415789
尝试在较小的子集上运行此代码,同时观察内存消耗情况。它会出现指数级的崩溃吗?还是有一个痛点阈值? - Hamish Grubijan
你的内存出现了问题吗?具体来说,当MemoryError被抛出时,你的代码中最有可能在堆栈底部的是哪一行?我没有看到任何理由,CLI不能够回收你在这里创建的对象。 - Adam Norberg
2
JPEG是一种压缩格式。当它被加载到内存中的位图时,它将被解压至全尺寸。 一个大小为350K的JPEG可能会在内存中扩展到1MB到10MB的位图,这取决于图像的复杂度。 图像的像素宽度和高度将告诉您每个图像在内存中需要多少内存(如果每像素32位,则为w * h * 4字节)。 - dthorpe
显示剩余3条评论
6个回答

14
很可能是因为您分配了许多未被释放的对象。
在这行代码中:
using (Bitmap T = new Bitmap(new Bitmap(Image.FromStream(ms))))
FromStream 方法调用会分配一个位图,而您永远不需要释放它。内部的 Bitmap 构造函数创建了另一个位图,而您也从未释放它。只有外部的 Bitmap 被释放。
请记住,using 块仅释放您在该语句中直接分配的对象,而不释放任何您创建并用作创建该对象参数的对象。
我认为从一个位图创建另一个位图的逻辑不太合理,因为您可以直接使用已加载的位图。
using (Bitmap T = (Bitmap)Image.FromStream(ms))

在这行代码中:
g.DrawEllipse(new Pen(Brushes.Red, 4), p.X - 5, p.Y - 5, 10, 10);

你创建了一个从未被释放的Pen对象。将它放入using块中:

Using (Pen red = new Pen(Brushes.Red, 4)) {
  g.DrawEllipse(red, p.X - 5, p.Y - 5, 10, 10);
}

2
这个应该不相关。对象很快就会变得无法访问,垃圾收集器应该能够将它们拾起,终结器处理它们,然后在下一次垃圾收集器扫描时将它们丢弃(终结对象不能立即被扫描)。我不明白为什么这会留下2GB的无法回收的资源。 - Adam Norberg
1
那么,.NET垃圾回收器在做什么? - user415789
5
垃圾回收器无法在它的终结器运行之前删除任何未处理对象,而且只有一个后台线程一个接一个地运行所有终结器。如果它无法以与创建图像相同的速率执行终结器,内存将会填满。 - Guffa
如果您处理所有可处理的对象,垃圾收集器就不必等待对象被终结。当需要清理未使用的对象以释放内存时,它可以自行执行。如果有太多对象等待完成终止,垃圾回收无法帮助释放内存,并且分配失败。 - Guffa
1
@HPT:当对象不再使用时,它们可以被收集,但这并不意味着垃圾回收会立即移除每个对象。对象会一直保留在内存中,直到它们被收集,通常是在下一次收集时。可释放对象包含非托管资源,因此必须在收集之前将其处理掉。您必须调用可释放对象上的 Dispose 方法(或使用 using 块),以便垃圾回收器可以删除它们。如果不这样做,垃圾回收器就会将它们交给终结线程,所以它们将一直处于使用状态,直到终结线程处理它们为止。 - Guffa
显示剩余8条评论

1

你的代码在内存中至少同时保留了4个位图的副本。位图B,内存流,可能有两个位图T的副本,以及可能还有一个Graphics g的副本。这些位图有多大?

B.Save之后,实际上不需要在内存中保留Bitmap B,但它会一直保留在内存中,直到构造它的using子句结束。你应该考虑重新排列代码,使得对象在不再需要时立即释放。至少将B.Save()后面的内容移出B using子句。

将位图保存到内存流似乎也有问题。你从磁盘加载位图到位图B中,然后将B保存到内存流中(SomeMethod调用不修改位图),然后再从内存流中加载位图。为什么?位图T应该与位图B相同。

还有new Bitmap(new Bitmap(Image.FromStream(...)))是怎么回事?这似乎浪费了内存。

尝试按照以下方式重新排列代码(未经测试):

while ((line = sr.ReadLine()) != null) 
{ 
    Bitmap T = null;
    try
    {
        MemoryStream ms = new MemoryStream()) 
        try
        { 
            Bitmap B = new Bitmap(line);
            try
            { 
                Point p = SomeMethod(ref c, new Point()); 
                B.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
            }
            finally 
            {
                B.Dispose();
            }

            T = new Bitmap(Image.FromStream(ms));
        }
        finally
        {
            m.Dispose();
        }

        using (Graphics g = Graphics.FromImage(T)) 
        { 
            g.DrawEllipse(new Pen(Brushes.Red, 4), p.X - 5, p.Y - 5, 10, 10); 
            FileInfo fi = new FileInfo(somepath2); 
            T.Save(Path.Combine(somepath3, fi.Name)); 
        }
    }
    finally
    {
        if (T != null)
        {
            T.Dispose();
        } 
    }
} 

这种安排允许像Bitmap B和memorystream M这样的对象尽快释放。您嵌套的using语句使它们的生命周期比需要的长得多,这将防止GC在所有这些过程中收集它们。如果GC在所有这些过程中启动,任何由于仍然被活动变量(或封闭using子句)引用而无法收集的对象都将被推到下一个较旧的堆代组,这意味着它们再次被考虑进行收集之前还需要更长的时间。

请注意,位图还涉及非托管资源-GDI位图句柄,它们消耗.NET GC系统外的系统内存,并且直到Bitmap.Dispose才会释放。因此,即使位图对象本身没有被GC收集,我们在执行流程中调用Bitmap.Dispose的事实也应该有助于减少内存压力,因为这将处理GDI位图句柄的处理。

我不认为这会完全解决您的内存问题,但它应该有所帮助。当处理固有涉及大量内存消耗的代码时,您需要更积极地管理分配和处理的时间。Using子句很方便,但是try..finally子句在我的看法中更明确、更精确。


1
你的修改没有改变任何东西。你的代码仍然强制在每次循环迭代期间保留4个或更多图像副本在内存中,而且由于垃圾回收器通常不会在函数退出之前启动,所以你的循环中会产生巨大的内存积累。 - dthorpe

1

看起来你正在编译或运行32位应用程序,或者使用了ANY CPU

将其编译为64位,就不会遇到32位应用程序的2GB进程限制。


@HPT - 你确定你正在以64位运行吗?Visual Studio是一个32位应用程序,使用它作为运行器(即在调试期间)将以32位运行您的应用程序。 - Oded
我知道,即使在调试时间或者运行发布版本时,同样的问题也会出现。 - user415789

1

由于所有的图像,您很可能会因内存不足而遇到问题。

我有一个小的屏幕截图应用程序,我一直在尝试它。当只处理100张图片时,它需要2GB以上的内存(物理内存+页面文件)。

尝试缩小规模,看看仅处理10张图片时会发生什么。


程序启动时占用约20MB内存,在调用“Run”函数后,内存使用量开始增加。如果我运行大约200个图像,则内存占用量会达到1.7GB左右,当“Run”函数完成工作并且内存使用量恢复到20MB时。 - user415789
1
这就是我一直看到的相同行为。 - Tony Abrams

0

GC.Collect()GC.WaitForPendingFinalizers()的使用并不是解决此问题的正确方法。您之所以会收到此错误,是因为您仍然拥有Bitmap对象。因此,您需要将其删除。

try
{
    IntPtr intPtrHBitmap = IntPtr.Zero; 
    BitmapSource _Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(intPtrHBitmap, IntPtr.Zero, System.Windows.Int32Rect.Empty, System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}
finally
{
    DeleteObject(intPtrHBitmap);
    _Source = null;
}

请务必将以下代码添加到类级别。
[System.Runtime.InteropServices.DllImport("gdi32.dll")]
public static extern bool DeleteObject(IntPtr hObject); 

0

我只是用 GC.WaitForPendingFinalizers() 替换了 GC.Collect(),问题就得到了解决。


他不应该同时调用GC.Collect()GC.WaitForPendingFinalizers()吗? - froeschli
你应该适当地处理对象,而不是等待终结器来完成它。 - Guffa

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