Click here to Skip to main content
Click here to Skip to main content
Go to top

Trouble with the Large Object Heap

, 16 Oct 2006
Rate this:
Please Sign up or sign in to vote.
Having trouble keeping your applications running? This could be why....

Sample Image - Cap-LOH-Burn.jpg

Introduction

Unlike most articles here at CodeProject that show you how to make something work, this one shows you how to make something break. Specifically, I am speaking of the Large Object Heap inside the .NET framework. Why you ask? Because like myself, you too might be experiencing this issue. If you are not yet, you might be tomorrow...

Details

In the problem domain I typically work (image processing), we need to load and display very large JPEG or TIFF images. They can range up to 500 MB when rendered as bitmaps. As I said, very large. The problem we encounter is that after a while, we would hit an OOM (out of memory) condition. It doesn't seem to matter how you manage yourself, if you need large objects you can fall victim to this issue.

If you study this topic on MSDN and the Internet, there are lots of good references to proper memory management practices. This article does not attempt to rehash that info here. Instead, it simply attempts to add a small footnote to your working knowledge set, as it relates to the CLR in all its glory.

Just to bring you up to speed with .NET and imaging. The mainstays of this problem space are the PictureBox control, the abstract Image class, as well as its derived class Bitmap. There are lots of great features available to you, and for most applications, everything will work fine. However, if you study the classes, you will note that in most cases, you do not have control over the underlying buffers used by these classes. Hence, their use of memory, and that my friends, leads us to how I found the weak spot in the CLR's heap management.

Within the managed heap, there are actually several heaps the CLR uses when allocating memory for managed objects. In general, these heaps can be divided into two classes, the SMO (small object heaps) and the LOH (large object heap). The small object heaps are fully managed by the CLR in quite an elaborate dance when studied in detail. Object lifetimes are tracked, object memory is freed as necessary, and finally, heaps are compacted if necessary to keep the available memory flowing so to speak. The LOH is treated to almost the same dance by the CLR with one exception. The LOH is never compacted. This is so, so to speak for performance reasons; after all, moving lots of objects around in memory can and does burn lots of CPU cycles.

Unfortunately, Microsoft has not provided a backdoor mechanism to force a LOH compaction cycle. So if you are like me and you simple must create and destroy lots of very large objects, your LOH might become fragmented. Once the LOH fragments, there is no way back from the edge, you must simply jump off. In other words, restart your process.

Here is a brutal application that illuminates the issue:

using System;
using System.Text;
using System.Threading;

namespace Burn_LOH
{
    class Program
    {
        static void Main (string[] args)
        {
            for (int count = 1; count <= 5; ++count)
                AllocBigMemoryBlock (count);
            Console.Write ("\nPress any key to exit...");
            while (Console.KeyAvailable == false)
                Thread.Sleep (250); 
        }

        static void AllocBigMemoryBlock (int pass)
        {
            const int MB = 1024 * 1024;
            byte[] array = null;
            long maxMem = 0;
            int arraySize = 0;
            MemoryFailPoint memFailPoint;
            while (Console.KeyAvailable == false)
            {
                try
                {
                    arraySize += 10;
                    array = new byte[arraySize * MB];
                    array[arraySize * MB - 1] = 100; 
                    maxMem = GC.GetTotalMemory (true);
                    Console.Write ("Pass:{0}  Max Array " + 
                        "Size (MB): {1:D4}  {2:D4}\r", 
                        pass, arraySize, 
                        Convert.ToInt32 (maxMem / MB));
                }
                catch (OutOfMemoryException)
                {
                    Console.Write ("\n");
                    maxMem = GC.GetTotalMemory (true);
                    if (arraySize < 20)
                        Console.Write ("Pass:{0} Small " + 
                            "Array Size (MB): {1:D4}" + 
                            "  {2:D4} {3}\r\n\n", 
                            pass, arraySize, 
                            Convert.ToInt32 (maxMem / MB), 
                            "Insufficient Memory...");
                    else
                        Console.Write ("Pass:{0} Failed " + 
                            "Array Size (MB): {1:D4}" + 
                            "  {2:D4} {3}\r\n\n", 
                            pass, arraySize, 
                            Convert.ToInt32 (maxMem / MB), 
                            "Out Of Memory...");
                    break;
                }
                finally
                {
                    array = null;
                    GC.Collect ();
                    GC.WaitForPendingFinalizers ();
                }
            }
        }
    }
}

As you can see from the code, the routine AllocBigMemoryBlock does all of the damage. This routine simply tries to create a bigger and bigger array until it reaches an out of memory condition. At that point, it writes what it has got to the console and returns. Specifically, it loops through a create / destroy code sequence, creating bigger and bigger arrays while trying to force the CLR to maximize the available memory. In all, not a very interesting piece of code.

Where things do get interesting is when you put the call to AllocBigMemoryBlock inside a loop and watch the LOH disapear. On my machine, it takes less than five cycles to loose all practical use of the LOH. Near the end of the program, it can't even allocate a 20 MB array!

Now, I know what you are thinking, memory is not being freed. Oh yea, try it! You will find your memory usage, VM usage, CLR memory management performance counters, et. al. all indicate there is no real memory being consumed by the program. So if the program doesn't have it, where is it?

Fragmented to bits is my best guess. However, it turns out that there is an issue with the heap manager in the 32 bit XP version of the 2.0 CLR. Under Vista RC1 and later, or under 64 bit versions of Windows, the issue does not arise.

If you would like to see some strange behavior, try replacing the main loop with the following code snippet. All it does is keep looping until you stop it. However, you will notice that the available memory comes and goes. On my machine, this cycle occurs about once a second. With the maximum array size I can create switching between 910 MB down to 10 MB, and back again.

static void Main (string[] args)
{
    int pass = 0; 
    Console.Write ("Press any key to stop...\n\n");
    while (Console.KeyAvailable == false)
    {
        ++pass;
        AllocBigMemoryBlock (pass);
    }
    Console.Write ("\nPress any key to exit...");
    Console.ReadKey ();
    while (Console.KeyAvailable == false)
    {
        Thread.Sleep (250);
    }
}

However, there is a workaround

Microsoft introduced a new class in the 2.0 framework, MemoryFailPoint. A MemoryFailPoint object is used to test if a memory allocation can succeed or not. Throwing an InsufficientMemoryException if an allocation of the requested size would result in an OutOfMemoryException. The interesting part is that so long as your program never generates an OOM condition, the heap continues to function as you would expect it to. To see this in action, replace the AllocBigMemoryBlock function with the following code snippet. The resultant program will continue to cycle, allocating as big of an array as your system will allow it to. Ah, once again all is well in la la land.

static void AllocBigMemoryBlock (int pass)
{
    const int MB = 1024 * 1024;
    byte[] array = null;
    long maxMem = 0;
    int arraySize = 0;
    MemoryFailPoint memFailPoint;
    while (Console.KeyAvailable == false)
    {
        try
        {
            arraySize += 10;
            using (memFailPoint = new MemoryFailPoint (arraySize))
            {
                array = new byte[arraySize * MB];
                array[arraySize * MB - 1] = 100; 
                maxMem = GC.GetTotalMemory (true);
                Console.Write ("Pass:{0}    " + 
                    "Max Array Size (MB): {1:D4}  {2:D4}\r", 
                    pass, arraySize, 
                    Convert.ToInt32 (maxMem / MB));
            }
        }
        catch (InsufficientMemoryException)
        {
            Console.Write ("\n");
            maxMem = GC.GetTotalMemory (true);
            if (arraySize < 20)
                Console.Write ("Pass:{0}  Small Array" + 
                    " Size (MB): {1:D4}  {2:D4} {3}\r\n\n",
                    pass, arraySize, 
                    Convert.ToInt32 (maxMem / MB), 
                    "Insufficient Memory...");
            else
                Console.Write ("Pass:{0} Failed Array Size" + 
                    " (MB): {1:D4}  {2:D4} {3}\r\n\n", 
                    pass, arraySize, 
                    Convert.ToInt32 (maxMem / MB), 
                    "Out Of Memory...");
            break;
        }
        catch (OutOfMemoryException)
        {
            Console.Write ("\n");
            maxMem = GC.GetTotalMemory (true);
            if (arraySize < 20)
                Console.Write ("Pass:{0}  Small Array" + 
                    " Size (MB): {1:D4}  {2:D4} {3}\r\n\n", 
                    pass, arraySize, 
                    Convert.ToInt32 (maxMem / MB), 
                    "Insufficient Memory...");
            else
                Console.Write ("Pass:{0} Failed Array" + 
                    " Size (MB): {1:D4}  {2:D4} {3}\r\n\n", 
                    pass, arraySize, 
                    Convert.ToInt32 (maxMem / MB), 
                    "Out Of Memory...");
            break;
        }
        finally
        {
            array = null;
            GC.Collect ();
            GC.WaitForPendingFinalizers ();
        }
    }
}

Conclusions

So what can I do, you ask?

If you can follow the rules of good CLR memory management:

  • Let the CLR manage the heap.
  • Don't force collection cycles.
  • Create object pools and reuse your big objects (the longer lived the better).

If you can't follow the rules of good CLR memory management:

  • Watch out!
  • Move to a version of Windows that works well for big objects.

But in either case, you should consider using MemoryFailPoint objects to protect your large objects from causing OOM conditions. Especially if your programs are designed to be long running. In that no matter what, if your heaps become unstable, there is no real way to recover them, short of restarting your process.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Keith Vinson
Chief Technology Officer Image Access, Inc.
United States United States
No Biography provided

Comments and Discussions

 
QuestionAny improvements in later versions of Windows or .NET? Pinmembersupercat910-Jun-09 10:43 
AnswerRe: Any improvements in later versions of Windows or .NET? PinmemberKeith Vinson10-Jun-09 11:57 
GeneralSame problem in .NET 1.1 PinmemberMohan Babu D7-Sep-07 9:28 
GeneralRe: Same problem in .NET 1.1 PinmemberKeith Vinson7-Sep-07 12:08 
QuestionAny patch? PinmemberMember #17063115-Jan-07 1:51 
AnswerRe: Any patch? PinmemberKeith Vinson15-Jan-07 4:32 
GeneralFor even more puzzling behavior PinmemberKeith Vinson17-Oct-06 13:23 
GeneralArithmetic operation resulted in an overflow. PinmemberRHarding_117-Oct-06 3:29 
GeneralRe: Arithmetic operation resulted in an overflow. PinmemberKeith Vinson17-Oct-06 5:22 
QuestionHave you submitted a bug to MS? PinmemberRHarding_117-Oct-06 3:18 
AnswerRe: Have you submitted a bug to MS? PinmemberKeith Vinson17-Oct-06 5:27 
GeneralCannot reproduce? [modified] Pinmemberpzand16-Oct-06 18:14 
GeneralRe: Cannot reproduce? PinmemberBil Irving16-Oct-06 20:35 
GeneralRe: Cannot reproduce? PinmemberChristian Klauser16-Oct-06 21:07 
GeneralRe: Cannot reproduce? PinmemberSteve Hansen16-Oct-06 23:02 
GeneralRe: Cannot reproduce? PinmemberKeith Vinson17-Oct-06 5:29 
GeneralRe: Cannot reproduce? PinmemberKeith Vinson17-Oct-06 5:30 
GeneralRe: Cannot reproduce? PinmemberAdis H.17-Oct-06 8:43 
GeneralRe: Cannot reproduce? PinmemberKeith Vinson17-Oct-06 8:56 
GeneralRe: Cannot reproduce? PinmemberAdis H.17-Oct-06 9:05 
GeneralRe: Cannot reproduce? [modified] PinmemberZoltan Balazs18-May-07 2:44 
GeneralRe: Cannot reproduce? PinmemberKeith Vinson18-May-07 4:48 
GeneralRe: Cannot reproduce? PinmemberZoltan Balazs18-May-07 5:34 
GeneralRe: Cannot reproduce? PinmemberBrian Hasden25-Jul-07 10:49 
GeneralRe: Cannot reproduce? Pinmemberdavethompsonwcc9-Oct-07 5:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140916.1 | Last Updated 16 Oct 2006
Article Copyright 2006 by Keith Vinson
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid