Memory Management

Memory Management

Traditionally, memory-related issues have been the impetus to a vast majority of bugs. Win32 processes, including managed applications that run in the Windows environment, own several assets. Of those assets, virtual memory is one of the most important. Win32 processes normally own four gigabytes of virtual memory, where the operating system resides in the upper two gigabytes. The upper two gigabytes are shared and protected. There is no reason to load the operating system for each instance of a Win32 process. The lower two gigabytes are private memory in which the application code, heaps, static data area, stack, and other aspects of the application are loaded. This memory is private and protected from access by other processes. Virtual Memory Manager (VMM), which is the kernel-level component of the NT Executive, guards private memory from incidental or deliberate external modifications.

The managed heap is created in the private memory of a managed application. There are several families of APIs that allocate memory from the available virtual memory, including the Heap APIs such as HeapCreate, HeapAlloc, and HeapFree. The Memory Mapped family of APIs include CreateFileMapping, MapViewOfFile, UnmapViewOfFile, and related functions. The Virtual APIs include VirtualAlloc, VirtualFree, and others. Internally, the Heap and Memory Mapped File APIs decompose to Virtual APIs.

Initially, the garbage collector (GC) calls VirtualAlloc with the MEM_RESERVE flag and reserves a block of memory. It then requests memory from the reserved area with successive calls to VirtualAlloc, but with the MEM_COMMIT flag. When committing memory, the first parameter of VirtualAlloc is the base address to a block of committed memory. The GC keeps the ending address of the previous memory allocation. With that information, it can calculate the base address of the next block of committed memory, which is stacked upon the previous allocation. This is a quick and efficient method of allocating memory. VirtualAlloc can be called with a null first parameter, which requires the VMM search for an appropriate location to commit memory, which is expensive. This is the syntax of VirtualAlloc:

  • LPVOID VirtualAlloc(LPVOID lpAddress, SIZE_T dwSize,

  • DWORD flAllocationType, DWORD flProtect)

The following is a stack trace and shows VirtualAlloc being called in a managed program. The first argument (0x00b54000) is the location of the allocation. This is where the memory is being committed.

0:000> kb
ChildEBP RetAddr Args to Child
0012e674 79e74391 00b54000 00001000 00001000 KERNEL32!VirtualAlloc
0012e6b4 79e74360 00b54000 00001000 00001000 mscorwks!EEVirtualAlloc+0x104
0012e6c8 79e74348 7a38b1b0 00b54000 00001000 mscorwks!CExecutionEngine::ClrVirtualAlloc+0x15
0012e6e0 79e8b7a4 00b54000 00001000 00001000 mscorwks!ClrVirtualAlloc+0x1b
0012e718 79e9f940 000000a8 00000001 00010000 mscorwks!UnlockedLoaderHeap::GetMoreCommittedPa
ges+0x90

0012e750 79e7f89f 000000a4 00000004 0012e794 mscorwks!UnlockedLoaderHeap::UnlockedAllocAlign
edMem_NoThrow+0x6c
0012e764 79e7f853 000000a4 00000004 0012e794 mscorwks!UnlockedLoaderHeap::UnlockedAllocAlign
edMem+0x15
0012e7a4 79e85010 0012e7d0 000000a4 00000004 mscorwks!LoaderHeap::RealAllocAlignedMem+0x40
0012e7f0 79e84eca 7a389bec 00000094 00000000 mscorwks!Stub::NewStub+0xc1
0012e834 79e8733c 7a389bec 00000000 00000000 mscorwks!StubLinker::Link+0x59

Reference Tree

The GC does not perform reference counting. Some memory models maintain a reference count on each memory object. When the count drops to zero, the object is immediately removed from memory. Overhead attributed to reference counting, especially for objects that are never reclaimable, is considerable. There are two benefits to the reference counting model. First, the cost of garbage collection is distributed across the life of the application. Second, it is proactive. Memory is reclaimed prior to being needed.

In managed code, a reference tree is erected when garbage collection is initiated, which avoids expensive reference counting. References no longer in the tree are assumed collectable and the memory for those objects is reclaimed. Memory is then consolidated and outstanding references are updated. This phase of memory management prevents fragmentation of the managed heap. The model described ignores finalization for the moment. The tree is not cached between garbage collection cycles. Rebuilding the tree is one reason why garbage collection is expensive. However, garbage collection is performed only when needed, which is a considerable efficiency.

An object is rooted when another object holds a reference to it. Conversely, root objects are not referenced by another object, including static, global, and local objects. C# does not support global objects. The root objects are the base of the object tree. The branches of the tree emerge from the root objects, as shown in Figure 13-8.

Memory Walkthrough

In this tutorial, the Store application is explored again. Three transactions are added and the root reference of each transaction is displayed.

  1. Start the Store application and add three transactions.

  2. Attach to the Store application with WinDbg.

  3. Transactions are instances of the Item class. Display information on the Item class using the !name2ee command:

    0:004> !name2ee store.exe Store.Item
    Module: 00d40c14 (Store.exe)
    Token: 0x02000002
    MethodTable: 00d442dc
    EEClass: 00db21c4
    Name: Store.Item
    
    
  4. With the MethodTable address, you can list the address of each transaction item. This is information is obtained with the dumpheap command:

    0:004> !dumpheap -mt 00d442dc
     Address       MT     Size
    013a03b8 00d442dc       20
    013b409c 00d442dc       20
    013bca10 00d442dc       20
    total 3 objects
    Statistics:
          MT    Count TotalSize Class Name
    00d442dc        3        60 Store.Item
    Total 3 objects
    
  5. Check the root of each object using the !gcroot command. This is a partial listing from the first Item object:

    0:004> !gcroot 013a03b8
    Note: Roots found on stacks may be false positives. Run "!help gcroot" for
    more info.
    ebx:Root:01392b60(System.Windows.Forms.Application+ThreadContext)->
    01392214(Store.Form1)->
    01392454(System.Collections.Generic.List`1[[Store.Item, Store]])->
    013d5f2c(System.Object[])->
    013a03b8(Store.Item)
    Scan Thread 0 OSTHread ee4
    Scan Thread 2 OSTHread c48
    DOMAIN(001483A8):HANDLE(WeakLn):9f1088:Root:013a074c(System.Windows.Forms.NativeMethod
    s+WndProc)->
    0139ec94(System.Windows.Forms.Control+ControlNativeWindow)->
    0139ebc4(System.Windows.Forms.CheckBox)->
    0139d6f8(Store.Transaction)->
    013a03b8(Store.Item)
    
  6. Are you curious about composition of the Item object? The items objects are shown as 20 bytes. What do those 20 bytes contain? Here is the source code for the Item class:

    public class Item: IDisposable
    {
        public Item()
        {
            ++nextPropId;
            propItemId = nextPropId;
        }
    
        public enum eProducts
        {
            Computer = 1,
            Laptop = 2,
            Printer = 4,
            Software = 8
        };
    
        private eProducts propProducts=0;
    
            public eProducts Products
            {
                get
                {
                    return propProducts;
                }
                set
                {
                    propProducts = value;
                }
            }
    
            static private int nextPropId=0;
            private int propItemId;
            public int ItemId
            {
                get
                {
                    return propItemId;
                }
            }
    
            public void Dispose()
            {
                --nextPropId;
            }
        
            private float[] buffer = new float[100];
        }
    

That was easy because the source code was available. What if the source code is not available? This is the normal case on a production machine. The !dumpclass command dumps the class. It uses the EEClass address, which is provided with !name2ee command. Actually, !dumpclass provides information that may be more valuable than the source code. You also receive state information. In the following listing, we are told that the static count is 3, which is the correct value at this moment.

0:004> !dumpclass 00db21c4
Class Name: Store.Item
mdToken: 02000002 (C:\store\Store.exe)
Parent Class: 790fa034
Module: 00d40c14
Method Table: 00d442dc
Vtable Slots: 5
Total Method Slots: 9
Class Attributes: 100001
NumInstanceFields: 3
NumStaticFields: 1
      MT    Field   Offset            Type   VT     Attr    Value Name
00d44224  4000001        8  System.Int32     0 instance           propProducts
790fe920  4000003        c  System.Int32     0 instance           propItemId
79129180  4000004        4  System.Single[]  0 instance           buffer
790fe920  4000002       24  System.Int32     0   static        3  nextPropId

Generations

The managed heap is organized into three generations and a large object heap. Generations are numbered 0, 1, and 2. New objects are placed in a generation or large object heap. Younger and smaller objects are found in the earlier generations, whereas older and larger objects are found in the later generations and the large object heap. This is efficiency by proximity. Objects that are apt to message other objects are kept close together in memory. This decreases page faults, which are costly, and the amount of physical memory required at any time.

Garbage collection in .NET is often described as nondeterministic, which means that memory recovery cannot be predicted. Garbage collection occurs when memory commits exceed the memory reserved for a particular generation. Because only a portion of the managed heap is being collected, this is more efficient. When an application starts, objects are allocated on Generation 0 first. Eventually, the memory available to Generation 0 is exceeded, which triggers garbage collection. If enough memory is reclaimed during garbage collection, the pending allocation is performed on Generation 0. If enough memory cannot be reclaimed, Generation 0 objects are promoted to Generation 1. This continues until Generation 0 and 1 are replete with objects. At that time, Generation 0 and 1 objects are promoted to Generation 1 and 2, respectively. By design, the older and larger objects tend to migrate toward the higher generations, whereas younger and smaller object are found in lower generations.

Memory on the managed heap is allocated top-down. The new objects are at the higher addresses. Generation 0 is at a higher address than Generation 1. 256 kilobytes, 2 megabytes, and 10 megabytes are reserved for Generation 0, 1, and 2, respectively. These thresholds may be adjusted. The GC changes these thresholds based on the pattern of allocations in the managed application.

As the name implies, the large object heap hosts large objects. Objects greater than 85 kilobytes (KB) in size are considered large objects. Instead of promoting these objects from one generation to another, which is costly, large objects are immediately placed on the large object heap at allocation.

Generations Walkthrough

This time the Store application has an Add Transactions button and an Add Large Transactions button. The Add Large Transactions button adds large transactions, which are instances of the LargeItem class. The LargeItem class inherits from the Item class and adds additional fields, such as the largeStuff field. The largeStuff field is greater than 85 KB and qualifies as a large object. The objective of this walkthrough is to determine the generation of each Item, LargeItem and largeStuff instance.

  1. Start the Store application. Add three regular transactions and four large transactions.

  2. Launch WinDbg and attach to the Store application.

  3. There should be three instances of the Item class in memory. Retrieve the method table address of the Item class with the !name2ee command. Then dump the Items instances using the !dumpheap -mt command:

    0:004> !name2ee Store.exe Store.Item
    Module: 00d40c14 (Store.exe)
    Token: 0x02000005
    MethodTable: 00d4431c
    EEClass: 00db22f4
    Name: Store.Item
    0:004> !dumpheap -mt 00d4431c
    Address       MT     Size
    013a8bd0 00d4431c       20
    013aabb0 00d4431c       20
    013aadd8 00d4431c       20
    013adffc 00d4431c       20
    013b513c 00d4431c       20
    013c1a4c 00d4431c       20
    total 6 objects
    Statistics:
          MT    Count TotalSize Class Name
    00d4431c        6       120 Store.Item
    Total 6 objects
    
  4. Unexpectedly, there are six instances, not three. Has a bug been uncovered?! This issue will be explored further later.

  5. Use the !name2ee command to obtain the method table address of the LargeItem. Dump the LargeItem objects. As expected, there are four objects:

    0:004> !dumpheap -mt 00d460a8
    Address       MT     Size
    013ab03c 00d460a8     5016
    013ae5a4 00d460a8     5016
    013be020 00d460a8     5016
    013ca900 00d460a8     5016
    total 4 objects
    Statistics:
          MT    Count TotalSize Class Name
    00d460a8        4     20064 Store.LargeItem
    Total 4 objects
    
  6. There should be four largeStuff fields—one for each LargeItem object. The easiest way to locate them by using is the !dumpheap -stat command. The objects are sorted by size, with the larger objects at the bottom of the list:

    0:004> !dumpheap -stat
    total 16356 objects
    Statistics:
          MT    Count TotalSize Class Name
    7b481adc        1        12 System.Windows.Forms.OSFeature
    7b47fd04        1        12 System.Windows.Forms.FormCollection
    7b47efec        1        12 System.Windows.Forms.Layout.DefaultLayout
    7ae86e80      134      5896 System.Drawing.BufferedGraphics
    790f8230      374      5984 System.WeakReference
    
    79124ec4       41      8136 System.Collections.Hashtable+bucket[]
    790fd688      341      8184 System.Version
    79116738      250      9000 System.Collections.Hashtable+HashtableEnumerator
    79124d8c        5     10596 System.Byte[]
    79110f78      523     12552 System.Collections.Stack
    7ae868e8     1049     12588 System.Drawing.KnownColor
    00d460a8        4     20064 Store.LargeItem
    7b47e850      522     33408 System.Windows.Forms.Internal.DeviceContext
    7910acbc     2104     42080 System.SafeGCHandle
    79124ba8      642     57496 System.Object[]
    00152760       14     77744 Free
    790fa860     7067     407316 System.String
    00e80838        4     16000128 System.Single[,]
    
  7. The last item in the report is a two-dimensional array of Single types. This is the largeStuff field. There are four instances shown. The first column of the command is the method table address. Dump the largeStuff instances using the !dumpheap -mt command:

    0:004> !dumpheap -mt 00e80838
    Address       MT     Size
    02396da8 00e80838  4000032
    027676c8 00e80838  4000032
    02b37fe8 00e80838  4000032
    02f08918 00e80838  4000032
    total 4 objects
    Statistics:
          MT    Count TotalSize Class Name
    00e80838        4  16000128 System.Single[,]
    Total 4 objects
    
  8. Finally, list the memory range for Generations 0, 1, and 2 and the large object heap. This is accomplished with the !eeheap -gc command:

    0:004> !eeheap -gc
    Number of GC Heaps: 1
    generation 0 starts at 0x013d2488
    generation 1 starts at 0x013af93c
    generation 2 starts at 0x01391000
    ephemeral segment allocation context: none
     segment    begin allocated     size
    0016bf58 7a74179c  7a76248c 0x00020cf0(134384)
    001687e0 7b45baa0  7b471f0c 0x0001646c(91244)
    00154b20 790d6314  790f575c 0x0001f448(128072)
    01390000 01391000  013ff3ac 0x0006e3ac(451500)
    Large object heap starts at 0x02391000
     segment    begin allocated     size
    02390000 02391000  032d9248 0x00f48248(16024136)
    Total Size  0x100cb98(16829336)
    ------------------------------
    GC Heap Size  0x100cb98(16829336)
    

Based on the addresses of the Item, LargeItem, and largeStuff instances, Table 13-8 maps each object to the managed heap. None of the instances resides in Generation 0.

Table 13-8: Item, LargeItem, and largeStuff Instances

Item

Address

No objects

N/A

Generation 0 Starts

0x013d2488

LargeItem4

0x013ca900

Item6

0x013c1a4c

LargeItem3

0x013be020

Item5

0x013b513c

Generation 1 Starts

0x013af93c

LargeItem2

0x013ae5a4

LargeItem1

0x013ab03c

LargeItem2

013ae5a4

Item3

0x013aadd8

Item2

0x013aabb0

Item1

0x013a8bd0

Generation 2 Starts

0x01391000

largeStuff4

02f08918

largeStuff3

02b37fe8

largeStuff2

027676c8

largeStuff1

02396da8

largeStuff2

027676c8

Large object heap

0x02391000

It is time to diagnose the earlier bug. Actually, there is no bug. The issue is nondeterministic collection. In the Store application, each transactions is initially an Item object. A LargeItem object is then initialized with the Item transaction. At that point, the Item object is no longer required, but the memory is not reclaimed. Therefore, there is a shadow item in memory for every LargeItem, which is the reason for the extra Item instances. At the next garbage collection, those items will be removed from memory. For demonstration purposes, there is a version of the Store application that forces garbage collection. It has a Collect Memory button that calls GC.Collect and removes the extra Item instances. When using this Store application, dump the Item instances before and after clicking the Collect Memory button to confirm that GC.Collect is reclaiming the extra objects. In general, calling GC.Collect is not recommended because it is an expensive operation.

Finalization

The finalization process has been ignored until now. It plays a vital role in garbage collection. Finalization also affects the performance and effectiveness of garbage collection. In C#, finalization is linked to class destructors. For C++ programmers, .NET presents an entirely different methodology for destructors.

Object.Finalize is the universal destructor in .NET. In C#, Finalize calls the class destructor. Destructors are called deterministically in C++. However, CLR calls destructors nondeterministically during garbage collection. Do not invoke destructors directly. Destructors are called as part of the garbage-collection process and are not called in a guaranteed sequence. In addition, you should clean up for only unmanaged resources in the destructor. For deterministic garbage collection, implement the IDisposable interface.

Destructors add processing overhead to an object. The overhead is incurred even before object is collected. The GC adds references for objects with destructors to the Finalization queue when the object is created. Thus, the extra overhead starts at the beginning of the object's lifetime.

Objects that have destructors but no outstanding references require at least two garbage collections to be reclaimed. During the first garbage-collection cycle, references to reclaimable objects are transferred from the Finalization queue to the FReachable queue, which is serviced by a dedicated thread. These objects are added to the list of objects already waiting on the FReachable queue to have their destructors called. The Finalization thread is responsible for invoking destructors on objects and then removing that object from the FReachable queue. When that happens, the object can be reclaimed and removed from memory during the next garbage collection.

The !finalizequeue command reports on objects waiting to have their destructors called. These are the objects on the FReachable queue.

Performance Monitor

The Performance Monitor has several counters that are helpful when debugging managed applications. Table 13-9 itemizes some of the more useful memory-related counters.

Table 13-9: Performance Monitor Counters

Name

Description

#GC Handles

The number of GC handles to external resources, such as windows and files.

# Bytes in All Heaps

Total number bytes allocated for Generation 0, 1, 2, and the large object heap.

# Induced GC

The peak number of times garbage collection was induced because of GC.Collect.

# of Pinned Objects

The number of pinned objects discovered during the last garbage collection.

# of Sinks Blocks in Use

A count of syncblock entries. (The syncblock is discussed in the section on debugging threads.)

#Gen 0 Collections

The number of times that Generation 0 has been garbage collected.

#Gen 1 Collections

The number of times that Generation 1 has been garbage collected.

#Gen 2 Collections

The number of times that Generation 2 has been garbage collected.

# Total Committed Bytes

The total number of virtual memory committed by the GC.

# Total Reserve Bytes

The total number of virtual memory reserved by the GC.

Gen 0 Heap Size

Maximum number of allocated bytes for Generation 0.

Gen 1 Heap Size

Maximum number of allocated bytes for Generation 1.

Gen 2 Heap Size

Maximum number of allocated bytes for Generation 2.

Large Object Heap Size

Number of bytes currently allocated for the large object heap.

%Time in GC

The percentage of time spent in garbage collection, which is updated at each garbage collection cycle.

Allocated Bytes/Sec

The number of bytes allocated per second, which is updated at each garbage collection cycle.

Finalization Survivors

The number of object that survived garbage collection and waiting for destructors to be called.

Gen 0 Heap Size

The maximum number bytes allocated for Generation 0.

Gen 0 Promoted Bytes/Sec

The number of bytes per second promoted from Generation 0 to 1.

Gen 1 Heap Size

The current number of bytes in Generation 1.

Gen 1 Promoted Bytes/Sec

The number of bytes per second promoted from Generation 1 to 2.

Gen 2 Heap Size

The current number of bytes in Generation 2.

Promoted Finalization-Memory from Gen 0

The number of bytes promoted to Generation 1 because of pending finalizers.

Promoted Finalization-Memory from Gen 1

The number of bytes promoted to Generation 2 because of pending finalizers.

Promoted Memory from Gen 0

Total bytes for objects that were promoted to Generation 1. This does not include the objects waiting on pending finalizers.

Promoted Memory from Gen 1

Total bytes for objects that were promoted to Generation 2. This does not include the objects waiting on pending finalizers.