GDI+的leak


GDI+自身是否有leak,我们不去管,现在说的是.NET代码中的处理。
首先看我这个简单的helper


using System;
using System.Diagnostics;
using System.Text;
using System.Runtime.InteropServices;

public class MemoryReport{
    [DllImport(
user32.dll, CharSet=CharSet.Auto)]
    
public static extern long GetGuiResources(IntPtr hProcess, long flag);

    
public static string Write(){
        Process p 
= Process.GetCurrentProcess();
        ing hcount 
= p.HandleCount;
        
long psize = p.PrivateMemorySize64;
        
long vsize = p.VirtualMemorySize64;
        
long workset = p.WorkingSet64;
        
long gcsize = GC.GetTotalMemory(false);
        
int gdiobjs = (int)(GetGdiResources(p.Handle,0));
        
int userobjs = (int)(GetGdiResources(p.Handle,1));

        
return String.Format(Handle count:{0:N0},Private Bytes:{1:N0}K, Virtual Bytes:{2:N0}K, Working Set:{3:N0}K, GC Heap Size:{4:N0}K, GDI Objects:{5:N0}, User Objects:{6:N0}, hcount, psize>>10, vsize>>10, workset>>10, gcsize>>10, gdiobjs, userobjs);
}

        
    }

现在我们做一个winform程序,放一个button,在click里面写如下测试代码:

for(int i=0;i<1000;i++){
    Bitmap b 
= new Bitmap(c:\1.bif);
    IntPtr ip 
= b.GetHbitmap();
    Bitmap b2 
= Bitmap.FromHbitmap(ip);
}


MessageBox.Show(MemoryReport.Write());

观察每次的结果,Private Bytes/ Virtual Bytes/ Working Set基本是一个上涨的走向。但是我们感兴趣的是这几个地方:
1、Handle count:这个值一般会波动变化,在这里例子里面,你把程序运行起来后,用taskmgr来观察Handle Count一栏(默认的没有,需要你自己手工添加这个column),一般是100以下。然后点一下按钮,handle count会增长1000左右,再点几次,会在1000上下波动,不会继续增长。
2、GDI Objects:这个值每次会增加1000
3、你连续点10次这个button,嘣!程序crash了。。。如果看dump里面的异常,会是什么bitmap的一个构造方法的parameter不正确。
4、GC Heap Size很小很小,我这里是2M。但是virtual size很大。

对于1,为什么这样,我不清楚;对于2,原因在于GetHbitmap返回的是一个Unmanged resource,GC不会回收(即使你使用了GC.Collect()这个值也不会下降的);对于3,OS默认的每个process的GDI objects上限为10000个,我们代码中是循环了1000次,所以如果你点了10次button,程序就会完蛋。对于4,说明leak的资源是unmanged resource,so,gc heap看起来很乖。

那么,如何修复上面的问题2?既然是unmanged resource,我们就要从unmanged找起。

[DllImport(“gdi32.dll”, CharSet=CharSet.Auto)]
public static extern IntPtr DeleteObject(IntPtr hobj);
for(int i=0;i<1000;i++){
    Bitmap b 
= new Bitmap(c:\1.bif);
    IntPtr ip 
= b.GetHbitmap();
    Bitmap b2 
= Bitmap.FromHbitmap(ip);

     DeleteObject(ip);
}


MessageBox.Show(MemoryReport.Write());

嗯,再运行一次,好了!GDI objects稳定了,再也没有变化过。
不过,我们修改一下循环计数器,到5000吧,然后观察Handle count,波动的比较厉害,内存相关的三组数值也稍有变化。好,我们再修改一次程序

[DllImport(“gdi32.dll”, CharSet=CharSet.Auto)]
public static extern IntPtr DeleteObject(IntPtr hobj);
for(int i=0;i<1000;i++){
    Bitmap b 
= new Bitmap(c:\1.bif);
    IntPtr ip 
= b.GetHbitmap();
    Bitmap b2 
= Bitmap.FromHbitmap(ip);

     b.Dispose();
     b2.Dispose();

     DeleteObject(ip);
}


MessageBox.Show(MemoryReport.Write());

重新run一次,嗯,这个世界终于清静了,handle count/gdi resource/ mem size都很平稳。

so,总结一下,对于类似上面的、可能被反复调用的type,如GDI+ obj,可以考虑使用完毕后立刻Dispose,这样可以被GC提早回收。对于返回一个IntPtr的方法,要仔细看,是不是需要再call win32里面对应的Delete方法。

对于绝大多数GDI+ obj,我们只需要DeleteObject即可,但是对于icon,我记着是另外一个函数,有兴趣的可以在msdn上查一下。

此条目发表在未分类分类目录,贴了标签。将固定链接加入收藏夹。

11 则回应给 GDI+的leak

  1. eparg说:

    如果不去调用Dispose,当对象被GC的时候,Finalizer会调用,那个时候handle会在Finalizer里面释放。
    但是GC发生的时机跟内存压力相关,而不是跟Handle数量相关。所以这里哪怕你Handle到了9999的极限,GC也不知道去给你清理。这就是GC内存管理模型不跟其它资源管理模型兼容的后果。凡是有Dispose方法的object,用玩了都赶紧去dispose吧
    另外你可以修改下你的程序,手动调用GC看是否还会崩溃

  2. juqiang说:

    多谢葡萄!

    我考虑过,所有的东西都要dispose,实在比较郁闷,哈哈!
    GC.Collect();之后再call gdi object leak那段,还是有问题。因为我想gc是不会管unmanaged resource的。

  3. eparg说:

    既然有Dispose,那么肯定定义了Finalizer
    GC会调用Finalizer的。你试试看

  4. juqiang说:

    我在家里的台式机上,没有vs,代码在笔记本上。回公司我试验一下。

  5. Anders Liu说:

    楼上两位都有道理,

    首先的确有Dispose方法的类型绝大多数都定义了Finalizer也就是析构器,而在这个析构器中肯定会释放非托管资源。

    但是GC.Collect的作用仅仅是“建议”运行时环境进行垃圾回收,但具体的回收实际是不可预测的,所以Collect方法可能不会马上进行回收。

    另外,即便是进行了回收,在第一次回收时,有Finalizer的对象会被放到一个“终止化可达列表”中,在下一次调用时才可能真正调用析构器。所以建议连续调用两次GC.Collect方法试验一下。

  6. Cliff说:

    但是对于icon,我记着是另外一个函数:DestroyIcon

  7. Lucifer说:

    对于GDI+等昂贵资源来说,当然应该在使用完之后就立即释放。

    也可以使用GC.AddMemoryPressure, GC.RemoveMemoryPressure以及HandleCollector类来处理。这个在Jeffrey Richter的CLR via C#有提及,楼主可以试验一下。

    此外,诚如Anders Liu所言,GC.Collect()不会立即回收内存,如果需要有Finalizer的对象释放,还要在GC.Collect()之后加上GC.WaitForPendingFinalizers()。

  8. juqiang说:

    楼上的cool!

    系统怀疑您的评论内容为广告,或者评论文字太短,请检查后重试!
    系统怀疑您的评论内容为广告,或者评论文字太短,请检查后重试!
    系统怀疑您的评论内容为广告,或者评论文字太短,请检查后重试!

  9. helixapp说:

    Lucifer有道理 里面道道还是挺多的!

  10. helixapp说:

    Lucifer有道理 里面道道还是挺多的!

发表评论

电子邮件地址不会被公开。 必填项已用*标注