Friday, December 13, 2024

Detect and avoid memory and resources leaks in .NET applications

 1 Introduction


Despite what a lot of people believe, it's easy to introduce memory and resources leaks in .NET applications. The Garbage Collector, or GC for close friends, is not a magician who would completely relieve you from taking care of your memory and resources consumption. It's important to understand leaks and how to avoid them, especially some 'key points'. You should be careful with these 'key points' when coding, because leaks are not the kind of things that is easy to detect automatically. They usually cost too much and may influence your project progress.

 

2 Prepare

 

2.1 Memory leaks

 

In computer science, a memory leak is a particular type of unintentional memory consumption by a computer program where the program fails to release memory when no longer needed. This condition is normally the result of a bug in a program that prevents it from freeing up memory that it no longer needs.

 

You should take it in heart that languages those provide automatic memory management, like Java, C#, VB.NET or LISP, are not immune to memory leaks.

 

2.2 Handles and resources

 

Memory is not the only resource to keep an eye on. When your .NET application runs on Windows, it consumes a whole set of system resources. Microsoft defines three categories of system objects: user, graphics device interface (GDI), and kernel.

 

User objects support window management: Accelerator tables, Carets, Cursors, DDE conversation(Dynamic Data Exchange Management Library), Hooks, Icons, Menus, Windows and Window position.

GDI objects support graphics: Bitmaps, Brushes, Device Contexts (DC), Enhanced metafile, Enhanced-metafile DC, Fonts, Memory DCs, Metafiles, Metafile DC, Palettes, Pen and extended pen, Regions, etc.

Kernel objects support memory management, process execution, and inter-process communications (IPC): Access to, Change notification, Communications device, Console input, Console screen buffer, Desktop, Event and Eventlog, Files and File mapping, Heap, Job, Mailslot, Module, Mutex, Pipe, Processes, Semaphore, Socket, Thread, Timer and Timer queue, Timer-queue timer, update resource, Window station, etc.

 

In addition to system objects, you'll encounter handles. Applications cannot directly access object data or the system resource that an object represents. Instead, an application must obtain an object handle, which it can use to examine or modify the system resource.

In .NET however, this will be transparent most of the time because system objects and handles are represented directly or indirectly by .NET classes.

 

2.3 Unmanaged resources

 

Resources such as system objects are not a problem in themselves, however the operating systems such as Windows have limits on the number of sockets, files, etc. that can be open simultaneously. That's why it's important that you pay attention to the quantity of system objects you application uses.

 

Windows has quotas for the number of User and GDI objects that a process can use at a given time. The default values are 10,000 for GDI objects and 10,000 for User objects. If you need to read the values set on your machine, you can use the following registry keys, found in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows: GDIProcessHandleQuota and USERProcessHandleQuota.

 

Given that these values can be customized, you may think that a solution to break the default limits is to raise the quotas. I think that this is a bad idea and the real solution should be that consume less resources anyway.

 

Quotas exist for a reason: your application is not alone on the system and it should share the system resources with the other processes running on the machine.

If you change them on your machine, they may be different on another one. You have to make sure that the change is done on all the machines your application will run on, which doesn't come without issues from a system administration point of view.

The default quotas are more than enough most of the time. If you find the quotas are not enough for your application, then you probably have some cleaning to do.

 

2.4 Desktop Heap

 

all the graphical applications of an interactive user session execute in what is named a "desktop". Every desktop object has a single desktop heap associated with it. The resources allocated to such a desktop are limited (but configurable).

 

 

Here is some suggestion to avoid desktop heap exhausted:

 

  • We use TabControls and we create the content of each tab on the fly, when it becomes visible;
  • We use expandable/collapsible regions, and again fill them with controls and data only when needed;
  • We release resources as soon as possible (using the Dispose method). When a region is collapsed, it's possible to clear it's child controls. The same for a tab when it becomes hidden;
  • We use the MVP design pattern, which helps in making the above possible because it separates data from views;

 

  • We use layout engines, the standard FlowLayoutPanel and TableLayoutPanel ones, or custom ones, instead of creating deep hierarchies of nested panels, GroupBoxes and Splitters (an empty splitter itself consumes three window handles...).

 

3 Experence

 

Recently, I have been working on a .NET project named 'Navigator'. The runtime is running correctly when you use it manually. But it crashes when running script file automatically. With the help of the tool procexp.exe, I observes that the GDI handles and USER handles increasing fast with the system running. And finally crashed the system.

 

3.1 First 'key points' (ToolTip

 

After research and analyze, the code snap below comes into my eyes.

 

    public class SomeClass {

        .

        .

        .

        public void SomeMethod() {

                        .

            .

            .

              ToolTip toolTip = new ToolTip();

              toolTip.SetToolTip(label, label.Text);

                        .

            .

            .

        }

            .

        .

        .

    }

 

Every time, system creates an instance for ToolTip when SomeMethod is called. This is the reason that cause lots of GDI handles. I update code above to this:

 

    public class SomeClass {

        .

        .

        .

        public void SomeMethod() {

                        .

            .

            .

              toolTip.SetToolTip(label, label.Text);

                        .

            .

            .

        }

            .

        .

        .

            ToolTip toolTip = new ToolTip();

    }

 

This time, GDI hanles issue is eradicated. But the USER handles still increase as before. I have to update the code again.

 

 

    public class SomeClass {

        .

        .

        .

        public void SomeMethod() {

                        .

            .

            .

              toolTip.RemoveAll();

              toolTip.SetToolTip(label, label.Text);

                        .

            .

            .

        }

            .

        .

        .

            ToolTip toolTip = new ToolTip();

    }

 

 

The USER handles increase slow down. But the issue still is there.

 

This is our first 'key point'. The Microsoft Inc. should update method SetToolTip above to static to avoid mistake. It is easy to induce people to create a new instance when call it every time. Finally, you should not forget call method RemoveAll to remove the reference on the labels to release their USER hanldles.

 

 

3.2 Second 'key points' (delegate | event

 

I feeling there must be some problem in other place. After some time, I take notice of code snap below

 

    public class SomeClass {

        .

        .

        .

        public void SomeMethod() {

                        .

            .

            .

              A a = new A(b)

                        .

            .

            .

              Controls.Clear();

              Controls.Add(a);

        }

            .

        .

        .

    }

 

    public class A {       

        public A(B b) {

                        .

            .

            .

              b.ElementsChanged += OnElementsChanged;

        }

            .

        .

        .

        private int OnElementsChanged(object sender, MenuModelEventArgs e) {

                .

            .

            .

        }

    }

 

In SomeMethod, it create a new instance of A to replace old one every time. But the old instance can not be collected by GC(garbage collector), since its method OnElementsChanged is regestered to the event ElementsChanged in object b. And b keeps alive through the whole life time of runtime. The problem is that the subscription to the ElementsChanged event creates a implicit reference from the b to the old panel.

Microsoft Inc. should fix this issue. No explicit reference means no useful. .Net platform should unregester the binded event and allow GC to collect it. So this is our second 'key point'. I must remove the binding event from old panel before replace it.

 

    public class SomeClass {

        .

        .

        .

        public void SomeMethod() {

                        .

            .

            .

              A a = new A(b)

                        .

            .

            .

              if (Controls.Count > 0) {

                A holdA = Controls[0] as A;

                holdA.RemoveElementsChangeEvent();

              }

              Controls.Clear();

              Controls.Add(a);

        }

            .

        .

        .

    }

 

    public class A {       

        public A(B b) {

                        .

            .

            .

              b.ElementsChanged += OnElementsChanged;

        }

            .

        .

        .

        private int OnElementsChanged(object sender, MenuModelEventArgs e) {

                .

            .

            .

        }

 

        public void RemoveElementsChangeEvent() {

            _groupItem.MenuModel.ElementsChanged -= _groupItem_Updated;

        }

    }

 

This time, the USER handle number is slow down advance. But the issue still exists. It is so unwieldy.

 

3.3 Third 'key points' (ContextMenuStrip | MenuStrip

 

It seems that code snap below is the cause.

 

    public class SomeClass {

        .

        .

        .

        public static void PopulateContextMenuStrip(ContextMenuStrip menuStrip) {

            menuStrip.Items.Clear();

            .

            .

            .

        }

        public static void PopulateMenuStrip(MenuStrip menuStrip) {

            menuStrip.Items.Clear();

            .

            .

            .

        }

            .

        .

        .

    } 

I suspect method Clear can not realease all USER handles when the menu level is more than 2(this is our case). So the code need some updates. 

    public class SomeClass {

        .

        .

        .

        public static void PopulateContextMenuStrip(ContextMenuStrip menuStrip) {

            ClearMenuStrip(menuStrip.Items);

            .

            .

            .

        }

        public static void PopulateMenuStrip(MenuStrip menuStrip) {

            ClearMenuStrip(menuStrip.Items);

            .

            .

            .

        } 

        private static void ClearMenuStrip(ToolStripItemCollection menus) {

            foreach (ToolStripItem item in menus) {

                ToolStripMenuItem menu = item as ToolStripMenuItem;

                if (menu != null) {

                    ClearMenuStrip(menu.DropDownItems);

                }

            }

            menus.Clear();

        }

            .

        .

        .

    } 

I add a new method ClearMenuStrip to remove all controls by recursion. Thanks the god, the leaks issue do not present any more. This is another bug for Microsoft Inc? I am not sure.

This is our third key point.

 

4 Common key points 

  • Static references
  • Event with missing unsubscription
  • Static event with missing unsubscription
  • Dispose method not invoked
  • Incomplete Dispose method  

In addition to these classical traps, here are other more specific problem sources: 

  • Windows Forms: BindingSource misused
  • CAB (Composite UI Application Block) : missing Remove call on WorkItem  

5 How to detect leaks and find the leaking resources

 

Even a little leak can bring down the system if it occurs many times. This is similar to what happens with leaking water. A drop of water is not a big issue. But drop by drop, a leak can become a major problem. 

  • There are usually three steps in leak eradication:
  • Detect a leak
  • Find the leaking resource
  • Decide where and when the resource should be released in the source code  

The most direct way to "detect" leaks is to suffer from them. You won't likely see your computer run out of memory. "Out of memory" messages are quite rare. This is because when operating systems run out of RAM, they use hard disk space to extend the memory workspace (this is called virtual memory).

 

What you're more likely to see happen are "out of handles" exceptions in your Windows graphical applications. The exact exception is either a System.ComponentModel.Win32Exception or a System.OutOfMemoryException with the following message: "Error creating window handle". This happens when two many resources are consumed at the same time, most likely because of objects not being released while they should.

 

Another thing you may see even more often is your application or the whole computer getting slower and slower. This can happen because your machine is simply getting out of resources.

 

If you suspect that objects are lingering in memory while they should have been released, the first thing you need to do is to find what these objects are.

This may seem obvious, but what's not so obvious is how to find these objects.

What I suggest is that you look for unexpected and lingering high level objects or root containers with your favorite memory profiler.

 

The next step is to find why these objects are being kept in memory while they shouldn't be. This is where debuggers and profilers really help. They can show you how objects are linked together.

 

It's by looking at the incoming references to the zombie object you have identified that you'll be able to find the root cause of the problem.Your goal should be to find the root reference. Don't stop at the first object you'll find, but ask yourself why this object is kept in memory.

 

6 Others

 

6.1 for Bitmap objects call DestroyIcon to release them

 

[DllImport("user32", EntryPoint = "DestroyIcon")]

private static extern void DestroyIcon(IntPtr handle);

using (System.IO.MemoryStream mstream = new System.IO.MemoryStream())

{

     bmp.Save(mstream, System.Drawing.Imaging.ImageFormat.Png);

 intP = new Bitmap(mstream).GetHicon();

 this.notifyIcon.Icon = Icon.FromHandle(intP);

     DestroyIcon(intP);

}

 

6.2 Call GC to collect memory forcely

 

// Force garbage collection.

GC.Collect();

 

// Wait for all finalizers to complete

GC.WaitForPendingFinalizers();


// Force garbage collection again.

GC.Collect();

No comments:

Post a Comment