Classic VB - Memory Leak Prevention and Detection
Disclaimer. This tutorial focuses heavily on GDI. Many other APIs, OCXs, DLLs and other references you add to your project may also present opportunities for leaking. However, most of the leaks we experience are those we created via GDI and GDI+ API calls. Therefore, focus has been set on GDI.
I have organized some of my thoughts and have tried to, as compactly as possible, provide many years of experience with finding leaks, into a short, step by step process. As with most potential problems, prevention is less painful than the cure. The first Section of this five-part document is a list of what I consider the best coding practices to preventing leaks in the first place. I think that if you have little experience tracking down leaks in your application, the following will help. There are other more complicated methods to help resolve leaks which include memory dumps and other more advanced techniques. I do not claim to be that smart, what I offer here is a commonsense, and somewhat easy and free, approach to dealing with the leaky situation.
A leaky application is a sure way for your application to lose all credibility with the user. Even worse, you, as a coder, may lose hard-earned credibility. I personally take pride in providing “leak-free” code. However, we all make mistakes, and the information within this document can help you correct those mistakes - it has helped me more times than I can remember and continues to.
API. Application Programming Interface (DLL)
Clean Up. Code used to destroy created objects, freeing memory back to the system
DC. Device Context
GDI. Graphics Device Interface (DLL)
IDE. Visual Basic's Integrated Development Environment.
Leak. Used or reserved memory that has not been freed and not returned to the system.
Here is an RTF file you can download to take with you. This FAQ was created with its information.
[VB6] Memory Leak Prevention and Detection
Section I. LEAK PREVENTION 1-6
The below is written with all operating systems in mind. Some causes of leaks in older operating systems no longer apply to newer operating systems. But as a coder, you probably want your project compatible with older operating systems too. Keep that in mind as you read through this list and should you say to yourself, “Well, that doesn’t apply in Vista.”
Leaks are almost always preventable. Good coding habits reduce chances of accidental leaks and makes finding those accidental leaks much easier. Below is a suggested list of good habits that can reduce the odds of your application leaking system resources. Also keep in mind that some APIs may have built-in, recognized, leaks. Avoid those and find workarounds. For example, TransparentBlt API leaks in older operating systems.
1. Do not use END statements in your code and do not click the Visual Basic IDE toolbar's End button (blue square icon). Many newcomers to Visual Basic tend to use the End statement and others will use it to allow termination out of some sort of tight or infinite loop. The bottom line is that the End statement is a poor choice and generally viewed by experienced programmers as an indication of immature or weak coding -- may sound harsh. There may be valid reasons to execute an End statement, to each their own. Here's a link to another tutorial discussing End and ways of closing your application without depending on End statements.
Originally Posted by MSDN
The above statement may be misleading. On one hand it says stuff will be destroyed but on the other hand it says code won't be executed. The underlined text above should be explained a bit more. Memory used by your application refers to memory that VB knows about (i.e., creating arrays, variables, collections, etc). This memory does not include memory bitmaps, DCs, pens, brushes, etc, that you have created via API calls. Memory created with VirtualAlloc, GlobalAlloc APIs may be released when the IDE is closed but remains until then, even if End is executed.
Originally Posted by MSDN
Since most visual basic custom drawn applications will place their clean-up code in the form, user control, or class Terminate events, this event is not called when an End statement is executed. Any clean up code never gets executed and memory objects are leaked. To prove it, simply open a new project, place a MsgBox "Terminated" in your form's Terminate or UnLoad event. Run the project and close it by pressing Alt+F4. Message box is displayed. Now run it again and click the IDE toolbar's End button -- no message box; and if any clean up code was there, it wouldn't have been executed either
2. When creating objects from APIs, with very few exceptions, delete or destroy those objects as soon as possible, or before your form, user control, or class terminates. At the end of this tutorial are the most common cases when you are not allowed to destroy an object you created.
3. Use the appropriate destruction or deletion API for the type of object you created. Tip. For public, global or module-wide variables, reset that variable to zero after it has been destroyed. It will make troubleshooting a bit easier should you find your project leaks. If it isn’t zero when your code terminates, it wasn’t released. Towards the end of this tutorial are the most common types of objects created from APIs and the related API function used to destroy those objects. Basically, you should have pairs of create and destroy statements for every object you create. It doesn't hurt to check the return value of the API function you are using to destroy the object. If that function returns zero, the function failed and you should determine why.
4. Never destroy an object that is currently selected into a DC. Always unselect the object from the DC, then destroy it. Never destroy or delete a DC while created objects are selected into it. Although not always required, suggest getting in the habit of caching/saving the return value of the often-used SelectObject API, and similar APIs (like SelectPalette, etc), so that you can unselect the object later with full confidence. The return value of those functions is the handle to the previous similar object that your new one replaced when it was selected into the DC.
5. Get in the habit of placing the appropriate destruction statement immediately after the line of code used to create an object. Then place the rest of your code between those two statements. If a routine will assign an object to some global, public, or module-wide variable, get in the habit of destroying that object before it is re-created. For example, if myBrush was a public variable, I could use something like:
6. Use On Error statements as much as possible. Many times objects that you created via APIs in a specific routine are also destroyed in that same routine. If you place your destruction code towards the end of the routine, it is helpful to use an On Error statement to send the routine to those destruction statements so they are always executed. This way, items can be destroyed even if the routine throws an error.
If myBrush <> 0 Then
' if it was selected into a DC, be sure to unselect it first, then
myBrush = CreateSolidBrush(brColor) ' now recreate it
' Then in the Terminate Event:
' Unselect any created objects from any DCs first, then
If myBrush <> 0 Then DeleteObject myBrush
Private Sub MyPaintRoutine()
On Error Goto ExitRoutine
‘ lines of code creating objects like DCs, bitmaps, etc
‘ some calculation routines
‘ no Exit Sub here because we want the clean up to occur
‘ lines of code that destroy what your routine created
If Err Then
‘ whatever you want to do if an error occurred
Memory Leak Prevention and Detection
Section I. LEAK PREVENTION 7-11
7. Check for the successful creation of objects. APIs will return a value of zero if the object was not created. Too many of us assume that a system has the memory and resources needed to create simple objects and don't check. Additionally, if some objects are created and others fail, you may be attempting to select objects into and out of DCs without success and when it comes time to destroy those objects, the destruction might fail. It is a good habit to test the return values. For example: If CreateCompatibleDC(0&) returns zero, you should handle the exception and abort your routine.
8. Go to www.MSDN.com and enter the API name, you are using to create objects, into the search engine. Open the page that describes that API and read through it. These pages are almost always short. Look for statements indicating whether or not you are responsible for destroying the object(s) created. You may not always be, or you may be responsible only in some situations. The MSDN pages tell you, a majority of the time, exactly which API to use for destruction.
Research the API on www.MSDN.com if you are unfamiliar with it. Never assume code you copied from some other source is correct. Some APIs will create objects that you are responsible for destroying and you may not even be aware of it and the author of the code you copied may not be aware of it either. GetIconInfo API is an example that can create up to two bitmaps that you are responsible for destroying.
9. Assume your code is used on some of the older operating systems and code appropriately. As mentioned above, TransparentBlt is leaky on Windows 95/98. Let’s say you really want to use TransparentBlt in your XP application but know it will leak in 95/98. You can find an alternative for the API and in your code test for the operating system version. When it comes time to run the API, check the operating system and if 95/98, reroute to your alternative method of TransparentBlt.
10. With VB controls, .Backcolor, .CLS, sizing and other properties/methods can cause an unexpected leak. Depending on the type of object (Form, UserControl, Picturebox, etc) and its AutoRedraw property, setting some properties may delete the control’s existing DC and create a new one. Guess what happened to those pens, brushes, fonts you selected into the DC before you changed that property? They are leaked because the DC was destroyed before you unselected those objects. Finish drawing as needed and remember to unselect and destroy items that were selected into the DC before changing control’s properties. If troubleshooting a leaky control, you can test whether this is happening in your drawing routines by noting the control’s hDC before the drawing routine starts and when it ends. See if the hDC changed and if so, determine where in that routine by stepping through the code as needed.
Proof: Add a button to a new form and on its click event copy & paste the below code. When the button is clicked you will see the 1st line of DCs are the same, but on the 2nd line, the DCs changed, which is a good reason why any coder should never cache a DC handle unless they know 100% for sure it will never change.
11. Take it from someone speaking from experience. When designing a custom drawn control, you may be creating dozens or even hundreds of objects during one single cycle of drawing. This is not uncommon when you are creating and destroying pens within a loop for example. If you have NT or XP, it is much easier to test your routines for leaks after each drawing routine has been finished rather than testing the entire project when you think it is finished. With the Task Manager that comes with NT and XP, you can easily detect a single GDI object leak (described in some detail in the next section).
Me.BackColor = vbButtonFace
Me.AutoRedraw = False
Me.BackColor = vbWhite
Debug.Print Me.hDC, “<< BackColor Changed w/o AutoRedraw set (DCs are same)“
Me.BackColor = vbButtonFace
Me.AutoRedraw = True
Me.BackColor = vbWhite ‘ changing this causes DC to be replaced if AutoRedraw=True
Debug.Print Me.hDC, “<< BackColor Changed with AutoRedraw set (DCs changed)“
‘ Try other property values or methods also: CLS and Width or Height for example
Not on the list, is a commonly accepted practice of setting to Nothing anything you had to create using the keyword Set. Many discussions on the web by true experts argue the logic, but the general consensus is that Visual Basic will take care of those objects during its internal garbage collection. However, there may be exceptions to the rule. If your application proves leaky and you have tried everything possible to find the leaks and are completely positive you are destroying everything you created, then suggest setting those objects to Nothing. For user controls (OCX files) and DLLs that return classes and/or objects, I would suggest using the "Set x = Nothing" approach when you are done with that object or class if the ocx/dll was not provided by Microsoft or from another trusted company. One exception appears to be DAO/ADO. Most will recommend explcitly setting their objects to Nothing as soon as you are finished with them.
Re: [VB6] Memory Leak Prevention and Detection
Section II. LEAK DETECTION
Once you determine your application is leaking, there is no simple way to magically go right to the leak and fix it. It takes a little detective work but really isn't that difficult. See the next section for hints on finding leaks after you know you have a leak. Of course the first bit of advice is to follow the good coding habits above. If you simply refuse to develop good coding habits, this section won't help you very much either.
1. If you have a Windows NT4 or newer operating system, you have at your disposal a good tool to help identify whether or not your application is leaking resources: the Windows Task Manager
a. First ensure your Task Manager is set up correctly (see image below)
(1) Open task manager. Either type taskmgr at a DOS prompt or right click on the Windows task bar.
(2) Click on the Processes tab
(3) Click on the menu item titled "View"
(4) Choose "Select Columns..."
(5) Ensure "GDI Objects" is checked
(6) Any other check boxes are at your preference
(7) Click the OK button -- done
b. Open your Visual Basic project in IDE.
(1) Prime the GDI count for the baseline to compare for leaks
- Close any code windows or design windows that are opened within the IDE
- Close the immediate/debug window
- Run your project by pressing Ctrl+F5
- Look to see if the debug window reappeared. If so, close that debug window.
- Close your application & return to IDE normally. DO NOT click the IDE toolbar's End button.
In this step we are allowing VB to create all of the needed objects it requires in order to run your project.
Typically, VB will add 100 or more to the count initially.
- Open Task Manager, click on the processes tab
- Find VB6.EXE in the "Image Name" column and highlight that row
- Ensure you can see the GDI Objects column.
(2) Get the baseline and test for leaks. The baseline is the number of GDI objects that your project should return to after your project is closed normally and you return to the IDE.
- Jot down the number of objects shown in the GDI objects column for VB6.EXE
- Run your project. The number of GDI objects will increase. This is normal.
- Give your project a good test. Click around a lot, open other forms, move over menus, etc. The GDI count will fluctuate.
- Close the application & return to IDE normally.
- Did number of GDI objects return to the baseline?
- (*) If not, no biggie yet. Try repeating this process again, up to two more times. Remember to note the new baseline.
- If the number does not return to the baseline, you have a leak somewhere. Let's assume it is with your code and not an API.
(*) Why up to two more times? I’ve have found that Visual Basic will add an extra one or two GDI objects as you start playing around with your running project. If this does happen, it is a one-time thing.
(**) For grins, run your app again and hit the STOP button on the IDE toolbar. Leaks? You bet!
2. If you have a Windows 9x system, you pretty much must rely on third party software or friends to help troubleshoot leaks. With these older systems, it can be very difficult to determine if you have a leak or not, especially small ones. Additionally, as mentioned in the section above, some APIs may leak on some systems, but not on others. If you researched your APIs, you should be aware of those and have found alternatives. So if a friend with NT/XP tests it for you and reports no leaks, then just assume you have no leaks unless your application generates "Out of Memory" errors. However, bigger leaks can be tested without third party software using one of the techniques below.
a. Option 1 (in IDE)
(1). Save your changes
(2). Add a test loop to your project
(3). Within the loop, call code that will create objects, classes or cause any user controls to redraw or refresh. If your custom objects accepts different fonts, colors, etc, ensure your loop changes those properties too.
(4) Run that loop several thousand times. Suggest upwards of 50,000 to 100,000 iterations
b. Option 2 (compiled)
(1). Open Visual Basic and set up a simple routine that will basically follow these steps
- Open the compiled application, get its hWnd so you can close it easily enough
- Wait a few seconds to ensure it is opened
- Close the application. Can be done with PostMessage WM_Close
(2). Have your test code loop several thousand times. The more the better.
c. If your system does not run out of resources at the end of the above tests, you can be pretty assured that you have no leaks or that any leaks you do have are tiny. If your application is leaking enough resources, you will eventually get an "Out of Memory" error.
Re: [VB6] Memory Leak Prevention and Detection
Section III. LOCATING LEAKS
So you have a leak? How do you find it?
1. The first suggestion, which can be used whether or not you have NT/XP, is to do a simple search:
a. Make a list of all the APIs you are using that create objects. You can use the listing in the next section
as a start, but that list is not all-inclusive. Therefore, hopefully, you researched your APIs so you know which ones are creating objects you should be destroying (Good Coding Habits #8
b. Start within any code page in your project (view the code). Press Ctrl+F and ensure the "Current Project" option button is selected. For each API name on your list, search your project for it. Once found, look for the line of code that is suppose to delete or destroy the object created by that API. If you can't find that line of code, then supply it and one leak is terminated. If the object created is global or public, look for the destruction statement in your Terminate or Unload event. If the object is locally declared in that routine, then the destruction statement should be in that routine. If the routine is returning a created object as a function parameter or return result, add that function/sub name to your list of APIs to search.
(1) Existing objects must be destroyed before they are re-created (Good Coding Habits #5
(2) If the object is selected into a DC, it must be unselected before it is destroyed. Failing to follow the "unselect-then destroy" order is a very common reason for many leaks (Good Coding Habits #4
c. Once you have identified all lines of code that create an object and have also identified the appropriate lines of code that delete or destroy those objects, test your project for leaks again. Rinse and repeat... Continue testing and correcting until all the leaks are gone.
2. For Windows NT and XP owners. The task manager can assist a bit more. Here are some tips. For Windows 9x owners, the following might offer a good place to start looking for leaks.
a. Is the leak in the hundreds as shown by the GDI count in Task Manager
? If so, look at any custom paint routines where you redraw a custom control or bitmap completely. Look inside loops that create pens and brushes or fonts. These are the most common places for such large leaks.
b. Is the leak only one, two or a few? If so, look at your global, public, or module-wide variables that are pointers to objects. Have you destroyed them in your Terminate or Unload event? This is the most common reason for very small leaks.
c. Let the project sit for a few seconds. Does the GDI count keep going up? If so, you have a timer that is calling a routine that is leaking or calling other leaky routines. Definitely a clue in the right direction to begin checking.
d. Move your mouse over any custom drawn controls without clicking. Does the count keep rising? If so, look at your mouse move event and the code that it contains or calls.
e. Is your project a custom drawn user control or object? If so, start a fresh project and add that control to your project. On a blank form, add only one of the controls. Test your project for leaks by following the steps in the previous section. Use that GDI leak count to determine where to start looking. If no leaks are present, change some of the user control's properties or settings that would create objects (i.e., font, picture, color properties) and try again. If you had a leak in the first project, you should have one in the new project. If not, then the leak may be in one of your other modules or classes in your first project.
2. If you cannot find the leak, you can try a more systematic approach to detection.
a. Try to place Exit Sub or Exit Function statements at the top of as many of your routines as possible and test a single routine at a time. If the routine doesn’t run, it can’t leak. When project is tested for leaks and none found, remove one of the remaining Exit statements and test the project again until all your added Exit statements are removed. If a leak is found, troubleshoot that routine to locate the leak before moving on to the next routine.
b. This isolation process may be impossible for many custom drawn control projects as they may have many routines that are dependent upon others, preventing isolating single routines. For such controls, it may be better to use Debug.Print statements in your routines and the Terminate event. Objects contained in individual routines should be easy enough to check visually since they should have a pair of creation and destruction API calls. Basically, for the non-local objects, you want to print whether or not global, public or module-wide variables are actually destroyed. Of course, immediately after the line of code that is suppose to destroy these objects, you should be resetting their variables to zero (Good Coding Habits #3
Basically add a Debug.Print “Created pen: “; [pen handle] for each GDI object you create and then Debug.Print “Destroyed pen “; [pen handle] for each GDI object you destroy. Change “pen” to the appropriate object type. When your program runs, you should have a pair of Created/Destroyed statements and if not, now you have a place to begin looking.
3. If all else fails… Pass your project off to a friend that has NT/XP and has a bit more experience in finding leaks. If the leaks are found, don't forget to thank them first, then ask them how they found it.
Re: Memory Leak Prevention and Detection
Researching your APIs is the only true way to know whether or not the API is creating anything you need to destroy. When researching, don't forget to review the structure information an API may use. For instance, the GetIconInfo API uses an ICONINFO structure and only reviewing the MSDN information for that API do you realize you must destroy something the API created. Looking at the page/link for ICONIFO you will find what members of that structure contain the handle(s) to the objects you need to destroy.
The following listings are not all-inclusive.
1. Common graphical objects. Not all of these API calls are from the GDI32.dll.
destroy with DeleteDC
destroy with DeleteEnhMetaFile
CreateCursor, LoadCursor, LoadCursorFromFile
CreateIconIndirect, CreateIcon,CopyIcon, DuplicateIcon,
ExtractIcon, ExtractIconEx, ExtractAssociatedIcon
Note that ExtractAssociatedIcon can return more than one icon
CreateCompatibleBitmap, CreateCompatibleBitmap, CreateDIBitmap,
CreateDIBSection, CreateBitmap, CreateBitmapIndirect, LoadBitmap
CreateSolidBrush, CreateBrushIndirect, CreateDIBPatternBrushPt, CreateHatchBrush, CreatePatternBrush
with DeleteObject (see Section V
below for exceptions)
CreateFont, CreateFontIndirect, CreateFontIndirectEx
CreatePen, CreatePenIndirect, ExtCreatePen
CreateEllipticRgn, CreateEllipticRgnIndirect, CreatePolygonRgn, ExtCreateRegion
CreatePolyPolygonRgn, CreateRectRgn, CreateRectRgnIndirect, CreateRoundRectRgn
with DeleteObject (see Section V
below for exceptions)
never destroy (see Section V
released with gdiplusShutdown
Note: Although tests seem to show DestroyIcon will destroy cursors and DestroyCursor will destroy icons, and DeleteObject may be used in place of other destruction functions, it is best to use the API function intended. The old saying applies: Use the right tool for the job.
2. Other APIs that can create objects that must be destroyed or should be released/unreferenced.
(see Section V
below for exceptions)
with DeleteObject if image is a bitmap
with DestroyCursor if image is a cursor
with DestroyIcon if image is an icon
the two bitmaps in the ICONINFO structure with DeleteObject
should always call ReleaseDC when done with it
it with CloseDesktop
it with DestroyWindow
GlobalAlloc, LocalAlloc, VirtualAlloc
with GlobalFree, LocalFree, VirtualFree respectively
3. Do not assume just because you passed an API a handle to some object you created, that the API will destroy it. Test the return value of the destruction API to ensure destruction. Again, simply researching MSDN on APIs you are not familiar with can help prevent leaky applications and also educate or enlighten you on the details of the API you are using.
4. If you noticed above, DeleteObject is used for many API functions that create GDI objects. If you find yourself being confused whether DeleteObject is destroying a palette, font, image, etc, there is nothing wrong with modifying your API declaration to suit your needs. For example, the following are valid declarations that may remove some of the confusion and you can add/rename it as many times as you need. Renaming APIs are common, just use the Alias keyword and place the real API function name in quotes as shown below. Worse case would be some of your fellow coders may look at you sideways. Don't alias an API to the same name as one of your routines.
' original API declaration
Private Declare Function DeleteObject Lib "gdi32.dll" (ByVal hObject As Long) As Long
' modified using Alias and naming it something more meaningful for you, including renaming the parameter
Private Declare Function KillRegion Lib "gdi32.dll" Alias "DeleteObject" (ByVal hRgn As Long) As Long
Private Declare Function KillPen Lib "gdi32.dll" Alias "DeleteObject" (ByVal hPen As Long) As Long
Private Declare Function KillBrush Lib "gdi32.dll" Alias "DeleteObject" (ByVal hBrush As Long) As Long
Private Declare Function KillFont Lib "gdi32.dll" Alias "DeleteObject" (ByVal hFont As Long) As Long
Memory Leak Prevention and Detection
The following listing is not all-inclusive
1. When using LoadImage API, you should not destroy anything you created if the LoadImage used the LR_Shared flag. Windows will destroy these when done with them.
2. When using CopyImage API, you do not have to destroy the original image you copied if you included the LR_CopyDeleteOrg flag with the call. That flag will destroy the original image for you.
3. When creating a region via the many region APIs, you must not destroy the region if you used SetWindowRgn with it. Windows will destroy it when done with it. In fact, you are not even supposed to reference that region handle again, ever.
4. I cannot think of any reason to destroy items contained in Visual Basic standard picture or standard font objects. Visual Basic does a real good job with these. However there is nothing wrong with setting the objects to Nothing or using LoadPicture(""), for images, before setting them to something else.
5. The GetStockObject API returns one of several permanent drawing objects. Do not destroy any returned by the API as they belong to the system.
6. If you use RegisterClass API to create a new window class, you may have also created a background brush for the class. If the class is successfully registered, the brush will be destroyed by windows when the class is eventually unregistered.
7. Some APIs may actually create objects that you don't need to be concerned with. For example the SelectClipRgn requires you to pass a region handle. It then copies the region and will destroy it when another region (actual or null) is selected into the DC or when the DC is destroyed. However, at some point in time, you must destroy the original region you passed to that API.