The Large Object Heap

I did not realize how unfamiliar many .NET developers are with the Large Object Heap (LOH) until we were interviewing a candidate for a dev position recently. He was explaining how .NET garbage collection happens and how allocations are done and the promotions from gen 0 to gen 1 to gen 2.

At this point, I asked the candidate whether objects were only allocated in the gen 0-2 heap or whether there was some other special place. To my shock, he said there were no other areas for allocation. So, I asked whether he has heard of the Large Object Heap. He said he hasn’t, so I gave him a brief description of the LOH as follows: Normally, objects are allocated in the manner you described, starting in gen 0 and getting promoted to gen 1 and 2 if they survive garbage collections in the previous generation. Object greater than 85,000 bytes though are a special case, and they are not allocated in gen 0. Instead, they get allocated in a special place known as the LOH and that location is also garbage collected. The second shocking moment was when the candidate replied: “oh yeah, well we never used the large object heap since we did not have any objects bigger than 85,000 bytes.” What? I certainly did not expect that answer.

This is why I decided I should write a short entry about the LOH. Hopefully it will help uncover more about the LOH than is normally known.

What is the Large Object Heap?

Before I start talking about the LOH, I want to point out an interesting thing that I noticed in the MSDN documentation for the LOH. The following article on MSDN, titled Memory Performance Counters, actually states the following:

Large Object Heap size

Displays the current size, in bytes, of the Large Object Heap. Objects greater than 20 KB are treated as large objects by the garbage collector and are directly allocated in a special heap; they are not promoted through the generations. This counter is updated at the end of a garbage collection, not at each allocation.

The amazing thing is that the threshold for considering an object in the LOH is incorrectly stated as 20 KB and that this statement went through the review process. The correct threshold is actually 85,000 bytes not 20 KB. In fact the precise threshold is 84,988 bytes as I will show later.

To demonstrate how objects are allocated and when they are allocated in gen 0 and when they are allocated on the LOH, I wrote the following the following simple program.

   1: namespace LargeObjectHeapSample
   2: {
   3:     class Program
   4:     {
   5:         // Define size as 1 byte less than the cutoff 
   6:         // value for allocating in the LOH.
   7:         const int ARRAY_SIZE_BYTES = 85000 - 13;
   9:         static void Main(string[] args)
  10:         {
  11:             byte[] bytes = new byte[ARRAY_SIZE_BYTES];
  12:             for (int i = 0; i < ARRAY_SIZE_BYTES; i++)
  13:             {
  14:                 bytes[i] = 0x20; // space
  15:             }
  16:         }
  17:     }
  18: }

In this version of the program, a byte array that is 84,987 bytes long is allocated. This is one byte less than the 84,988 bytes threshold for allocating the object in the LOH. So, let’s see where the object gets allocated.

To illustrate the allocations, I will debug the program in Release mode, and break into it after the bytes variable is initialized. To do that, there are two things that I do first:

  • I always make sure that the “Suppress JIT optimization on module load (Managed only)” setting is unchecked. This setting is found under Tools –> Debugging –> General.

Click to view larger image

  • I also make sure that the “Enable unmanaged code debugging” setting is checked under my project’s Debug settings for the Release configuration since I will be debugging the program in Release mode and while optimized.

Click to view larger image

The previous steps will help prepare my simple program for debugging in Release mode using the managed debugger extensions in sos.dll. SOS (which stands for Son of Strike) is a debugger extensions DLL that ships with each version of the CLR. There are currently three versions of sos.dll. The first is for version 1.1 of the CLR, the second is for version 2.0 of the CLR, and the third is for version 4.0 of the CLR.

The following screenshot shows the SOS.DLL version that I will be using for this program since I am building it against .NET Framework version 3.5 (.NET Framework v2.0, v3.0, and v3.5 all use CLR v2.0):

Click to view larger image

For CLR v 2.0, you can find SOS.DLL at the following location:


or %windir%\Microsoft.NET\Framework\v2.0.50727

If you are writing applications against version 4.0 of the CLR, you will find the corresponding version of SOS.DLL at the following location:


or %windir%\Microsoft.NET\Framework\v4.0.30128

Click to view larger image

Now that my program is ready for debugging and inspecting its memory allocations, I can start debugging it. I will first place a breakpoint at the end of the program, after the bytes variable has been initialized.

Click to view larger image

Now, all I need to do is press F5. As soon as the program runs and reaches the breakpoint, it is ready for inspection. Here are the steps I follow to inspect the allocations:

  • Open the Immediate window.
  • Type: .load sos

You should see the following line printed in the Immediate window: “extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded”.

Note that if you have not checked the “Enable unmanaged code debugging” project setting, you would have seen the following message printed instead: “SOS not available while Managed only debugging.  To load SOS, enable unmanaged debugging in your project properties.”.

  • Type: !eeheap –gc

The output will now look something like the following screenshot:

Click to view larger image

eeheap is a command provided by SOS that shows general information about heaps used by the process. SOS commands are prefixed with the bang character (the exclamation mark). In the above, I specified the –gc parameter to limit the results to only the garbage collector. By default, the command shows the heaps for the GC and the loader. In my case, I am not interested in the loader heap.

Notice that the output shows the starting addresses for generations 0, 1, 2, and for the Large Object Heap. It also shows the total size of each heap. The total size of generations 0, 1, and 2 (sometimes called the Small Object Heap or SOH) is shown as 531464 bytes. The total size of the LOH is shown as 8784. Hence, it is obvious that our large object (the bytes variable) was not allocated on the LOH. Instead it was allocated in generation 0 in the SOH. This can be confirmed by inspecting the contents of generation 0 as shown in what follows.

  • Type: !dumpheap 0x01ca1018

The 0x01ca1018 address is the starting address for generation 0 as shown in the results of the !eeheap –gc command.

Click to view larger image

The dumpheap command will display a long list of objects which are the contents of generation 0. Scrolling down the list, you will notice an object instance with a size of 85000 bytes.

Click to view larger image

Since we have allocated 84,987 bytes for our “bytes” variable, we can conclude that the object at address 01d0c40c is indeed our variable:

Address       MT     Size

01d0c40c 6c7db314    85000

In order to confirm that, we use the dumpobj (or do for short) command. It is used to dump the heap object at a given heap address.

!dumpobj 01d0c40c

Click to view larger image

From the object you can notice two things:

  1. The internal object contents are indicated as an array of 84987 elements, as we have expected.
  2. The total object size is 84999 bytes.

Since the program is running on a 32-bit system, the data size is 4 bytes. Hence, allocating an object that is 11 bytes long actually consumes 12 bytes. That is why dumpheap shows a size of 85000 bytes and not 84999 as dumpobj shows.

In fact, the size that dumpheap shows is what would be returned from calling the objsize command:

!objsize 0x01d0c40c
sizeof(01d0c40c) =        85000 (     0x14c08) bytes (System.Byte[])

So where did the additional 12 bytes come from?

It turns out that each object has a 4-byte method table address at its beginning. For an array object, this is followed by a 4-byte object size (number of elements). Let’s take a look at this a bit more deeply. To do this, we are going to use the U (unassemble) command from SOS and run it against the object address we have been using (0x01d0c40c). This is similar to using the Disassembly window. Here are the results:

!u 01d0c40c
Unmanaged code
01D0C40C 14B3             adc         al,0B3h
01D0C40E 7D6C             jge         01D0C47C
01D0C410 FB               sti
01D0C411 4B               dec         ebx
01D0C412 0100             add         dword ptr [eax],eax
01D0C414 2020             and         byte ptr [eax],ah
01D0C416 2020             and         byte ptr [eax],ah
01D0C418 2020             and         byte ptr [eax],ah
01D0C41A 2020             and         byte ptr [eax],ah
01D0C41C 2020             and         byte ptr [eax],ah

In the above, only the first 10 data records are shown. U is typically used to unassemble code at a certain address (typically the address pointed to by the EIP or extended instruction pointer register). Since we are using it to inspect an object instance, the interpretation of the bytes as instructions is irrelevant to us since we know we are inspecting data. What we really want is to inspect the raw object structure.

We can easily notice that starting at address 01D0C414, we start seeing a series of bytes with the value 20 (hex). This is the same 0x20 that we have initialized our array members with. So, we can see that the actual data starts at address 01D0C414. This is 8 bytes after the starting address that we had, 0x01d0c40c. So, there are 8 bytes at the beginning of the object that are not data. What are they?

Notice from the dumpobj output the MethodTable value of 6c7db314. This is actually the first 4 bytes we see above in the object. Here they are:

01D0C40C 14B3             adc         al,0B3h
01D0C40E 7D6C             jge         01D0C47C

Or if we break down the addresses, we get:

01D0C40C 14
01D0C40D B3
01D0C40E 7D
01D0C40F 6C

What about the next 4 bytes? If we try to assemble them the same way we did with the Method Table address, we get the following:

01D0C410 FB
01D0C411 4B
01D0C412 01
01D0C413 00

So, the value is 00014BFB. This is actually 84987 decimal. And as you can notice, this is the number of elements in our byte array. So, the second piece of information that the object stores is the number of elements.

The rest of the information about the object structure can be retrieved from the method table, using the method table address that we already have. For this, we will use the dumpmt (dump method table) SOS command:

!dumpmt 6c7db314
EEClass: 6c59b9b8
Module: 6c571000
Name: System.Byte[]
mdToken: 02000000  (C:\Windows\assembly\GAC_32\mscorlib\\mscorlib.dll)
BaseSize: 0xc
ComponentSize: 0x1
Number of IFaces in IFaceMap: 4
Slots in VTable: 25

Now that we have been able to account for most of the additional size we see associated with our object instance, we still notice that there are 4 bytes left unaccounted for. Looking at the object map, there is nothing left. We have the first 8 bytes, followed by the array elements, 84995 bytes. So where are the other 4 bytes mentioned in dumpobj’s output?

To find those, there is one additional piece of information we need to know about the way CLR objects are allocated. In addition to the object structure described above (the method table address, the array size, and the actual data), the object structure actually includes 4 bytes that precede the object address itself. The CLR heap’s object allocation hence consists of those first 4 bytes, called the sync block address, followed by the rest of the object metadata and contents described above. Sync blocks are the internal representation of lock/Monitor objects. You can find out if there are sync block entries by using the syncblk SOS command, as shown below:

Index SyncBlock MonitorHeld Recursion Owning Thread Info  SyncBlock Owner
Total           0
CCW             0
RCW             0
ComClassFactory 0
Free            0

As noticed, there are no entries in the sync block table.

Furthermore, we can look at the sync block address entry for our byte array, by expanding the U command’s results to start 4 bytes before the actual object address, as follows:

   1:  !u 01d0c408
   2:  Unmanaged code
   3:  01D0C408 0000             add         byte ptr [eax],al
   4:  01D0C40A 0000             add         byte ptr [eax],al
   5:  01D0C40C 14B3             adc         al,0B3h
   6:  01D0C40E 7D6C             jge         01D0C47C
   7:  01D0C410 FB               sti
   8:  01D0C411 4B               dec         ebx
   9:  01D0C412 0100             add         dword ptr [eax],eax
  10:  01D0C414 2020             and         byte ptr [eax],ah
  11:  01D0C416 2020             and         byte ptr [eax],ah
  12:  01D0C418 2020             and         byte ptr [eax],ah

In the above, the sync block address for the byte array starts at address 01D0C408 (line 3). Notice that it is 0x00000000. This indicates that our object does not have a sync block entry since this is not a valid address.

Inspecting Objects in the Large Object Heap

Now, let’s modify our program so that the byte array is 84988 bytes longs, one byte longer than our previous run, and let’s see whether the object makes it to the LOH this time.

Click to view larger image

Notice this time that the LOH size is much bigger than the previous run. It looks like our byte array made it to the LOH.

Large object heap starts at 0x02ce1000
 segment    begin allocated     size
02ce0000 02ce1000  02cf7e68 0x00016e68(93800)

Let’s confirm this by inspecting the contents of the LOH. The dumpheap command comes to the rescue again:

   1:  !dumpheap 0x02bf1000
   2:   Address       MT     Size
   3:  02bf1000 003f4650       16 Free
   4:  02bf1010 6c7b4ed0     4096     
   5:  02bf2010 003f4650       16 Free
   6:  02bf2020 6c7b4ed0      528     
   7:  02bf2230 003f4650       16 Free
   8:  02bf2240 6c7b4ed0     4096     
   9:  02bf3240 003f4650       16 Free
  10:  02bf3250 6c7db314    85000     
  11:  02c07e58 003f4650       16 Free
  12:  total 9 objects
  13:  Statistics:
  14:        MT    Count    TotalSize Class Name
  15:  003f4650        5           80      Free
  16:  6c7b4ed0        3         8720 System.Object[]
  17:  6c7db314        1        85000 System.Byte[]
  18:  Total 9 objects

From line 10, we can take the object address and use dumpobj to get the object details:

!do 02bf3250
Name: System.Byte[]
MethodTable: 6c7db314
EEClass: 6c59b9b8
Size: 85000(0x14c08) bytes
Array: Rank 1, Number of elements 84988, Type Byte
Element Type: System.Byte

The same discussion we had above still applies, except that this time, the object lives in the LOH rather than generation 0 in the SOH.

We can also use U to dissect the object further. This time, we will start straight from the “real” beginning of the object, i.e. 4 bytes before the start address:

   1:  !u 02bf324C
   2:  Unmanaged code
   3:  02BF324C 0000             add         byte ptr [eax],al
   4:  02BF324E 0000             add         byte ptr [eax],al
   5:  02BF3250 14B3             adc         al,0B3h
   6:  02BF3252 7D6C             jge         02BF32C0
   7:  02BF3254 FC               cld
   8:  02BF3255 4B               dec         ebx
   9:  02BF3256 0100             add         dword ptr [eax],eax
  10:  02BF3258 2020             and         byte ptr [eax],ah
  11:  02BF325A 2020             and         byte ptr [eax],ah
  12:  02BF325C 2020             and         byte ptr [eax],ah

Notice that, as before, the sync block address was 0x00000000 (lines 3 and 4). Also, the rest is also still the same as before.

More Information

The discussion presented above was done in Visual Studio 2010 Release Candidate’s IDE. It can be done the same way in VS2008. This can also be done in WinDBG. In WinDBG, you will also get some additional commands that you can use and that are built into WinDBG.

There are many resources on the web describing the Large Object Heap and other aspects of the garbage collector. My favorites resources are the blogs of Maoni Stephens and Tess Ferrandez.

Maoni’s blog:

Tess’ blog:

I always learn something new whenever I read a blog entry on those blogs.

Maoni also wrote an article for MSDN Magazine a couple of years ago which is a very good examination of the LOH. It provides very good detail, and I would highly recommend it to anyone interested in learning more details about the LOH.

This is only an introduction to the LOH. I tried to present it in a simplified manner, so hopefully it will be helpful as an intro to that area of the GC heap. Enjoy!

Published 02-21-2010 10:14 PM by Mohammad Jalloul
Powered by Community Server (Non-Commercial Edition), by Telligent Systems