Out of memory! Are you nuts!?

I may have said this before (my memory's not what it was prior to decade number 4), but the most common issues I debug are memory issues. These range from high memory issues (categorized as 800MB of more memory in use by the worker process) to OutOfMemory exceptions, or OOM. High memory cases don't usually accompany any questions from customers other than "what the heck is taking so much memory?", but OOM conditions are often accompanied by "Why am I seeing OOM when the process is using 1GB of memory on a box with 4GB of memory available?" That's a good question, and fortunately, that question has a good answer as well.

Before we go into why you might see an OOM exception, you first need to understand the basics of how we allocate memory in the .NET world. We allocate memory in large contiguous blocks. Any objects you create are created in our heaps that are located within the memory segments we've allocated. As long as there is sufficient free space in the managed heap for your object, we won't allocate more memory from the OS. As objects are freed from gen 0, 1, and 2, we compact the heap so that you don't run into any problems with memory fragmentation in those heaps. The large object heap (LOH), which is where any object that's 85K or larger lives, is never compacted. However, we do maintain a list of free blocks in the LOH segments, and we'll reuse free blocks if we can. Therefore, fragmentation in the LOH is rarely an issue.

With that out of the way, suppose you need to allocate an object that is 40K in size and we don't have enough memory on the gen 0 heap to create that object. We'll then go to the OS and attempt to allocate another contiguous segment. If we're unable to get a segment of contiguous memory from the OS, we'll throw an OOM exception.

Another scenario: suppose that GC runs and we free all of the objects within a particular segment. We will then release that segment back to the OS. Now let's say that 5 minutes later, we need to grow the heap again. We go back to the OS to get a new segment, but some other process has placed a 1K allocation right at the beginning of that segment that we released. What happens? Well, if that 1k allocation fragments your memory and we're unable to find contiguous space . . . you guessed it. An OOM occurs.

So how do you avoid seeing this kind of thing? The first thing you can do is try and avoid a high memory situation. Almost all of the OOM cases we see involve high memory as well. That's because as you approach 80% of available memory for the process (and that's 2GB on a 32-bit machine without the 3GB switch enabled), the likelihood of seeing an OOM begins to increase exponentially. The reason for this (based on the information above) is pretty obvious. If you have reached the 80% mark, the chances of us finding a large, contiguous chunk of memory go down pretty substantially due to fragmentation. How do you avoid a high memory situation? There are a lot of answers to that question, but a few are:

  • Don't create a large number of objects on a per-request basis
  • Don't store large objects in Session
  • Don't cache user-specific data
  • Call Dispose() on objects that provide a Dispose() method
  • Call Marshal.ReleaseComObject in a loop on your COM objects (FinalReleaseComObject without the loop in 2.0)

I don't mean for that to be an exhaustive list, but you get the drift.

Another thing that you want to do to avoid OOM situations is to avoid fragmentation. If system memory is fragmented, it becomes more difficult for the CLR to find contiguous memory to grow the managed heap. There are also a lot of causes of memory fragmentation, but I'd have to say that one of the most common we see is caused by dynamic assemblies. (See my blog entry on the Debug attribute.) Other causes of dynamic assemblies are:

  • XML serialization (We have an article on this)
  • Script blocks in XSL transforms (We have an article on this, too)
  • Heavy use of regular expressions
  • Creating your own dynamic assemblies en masse

There are a lot of other reasons for fragmentation as well, but these are among the most common.

I hope this helps to explain why OOM conditions occur and how you can avoid them.