Memory Management

Historically, memory-related issues are the origin of many bugs. Win32 processes, including managed applications, own resources. Of those resources, virtual memory is one of the most important. Win32 processes normally own 4 gigabytes (GB) of virtual memory, where the operating system resides in the upper 2 GB. The upper 2 GB are shared and protected from user mode access. The lower 2 GB are private memory, where the application code, heaps, static data area, stack, and other sections of the individual application are loaded. This memory is protected from access by other processes. The Virtual Memory Manager (VMM), which is the kernel-level component of the NT Executive, guards private memory from incidental or deliberate changes from other applications.

The managed heap is created in the private memory of a managed application. There are several families of native application programming interfaces (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, CreateFileMapping, MapViewOfFile, UnmapViewOfFile, and related functions, also allocate memory. Finally, the Virtual APIs, including VirtualAlloc and VirtualFree, allocate memory dynamically at run time. Internally, all memory allocations, including Heap and Memory Mapped APIs, decompose to Virtual APIs.

VirtualAlloc is used to allocate memory in contiguous memory. Here is the syntax of VirtualAlloc:

LPVOID VirtualAlloc(LPVOID lpAddress, SIZE_T dwSize,
    DWORD flAllocationType, DWORD flProtect)

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

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+
0012e6e0 79e8b7a4 00b54000 00001000 00001000 mscorwks!ClrVirtualAlloc+0x1b
0012e718 79e9f940 000000a8 00000001 00010000 mscorwks!UnlockedLoaderHeap::GetMoreCommitt
0012e750 79e7f89f 000000a4 00000004 0012e794 mscorwks!UnlockedLoaderHeap::UnlockedAllocA
0012e764 79e7f853 000000a4 00000004 0012e794 mscorwks!UnlockedLoaderHeap::UnlockedAllocA
0012e7a4 79e85010 0012e7d0 000000a4 00000004 mscorwks!LoaderHeap::RealAllocAlignedMem+0x
0012e7f0 79e84eca 7a389bec 00000094 00000000 mscorwks!Stub::NewStub+0xc1
0012e834 79e8733c 7a389bec 00000000 00000000 mscorwks!StubLinker::Link+0x59

Object graph

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

In managed code, an object graph is built when garbage collection is initiated, which avoids expensive reference counting. Objects without references in a tree are assumed to be collectable, and the memory for those objects is reclaimed. Memory then is consolidated and outstanding references are updated. This phase of memory management prevents future fragmentation of the managed heap. (We are ignoring finalization for the moment—we will cover this topic in the section "Finalization," later in this chapter.) The object graph is not cached between garbage collection cycles. Rebuilding the trees at each garbage collection cycle 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 rooted object holds a direct or indirect reference to it. Rooted objects are not referenced by another object and reside at the base of an object graph. Rooted objects include static, global, and local objects. C# does not support global objects.

Memory Example

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

  1. Start the Store application in the memory subfolder and add three transactions.

  2. Attach to the Store application with WinDbg. Load SOS.

  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. Use the dumpheap command and the MethodTable address to list the address of each transaction item:

    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. Confirm the root of each object using the !gcroot command. This shows how the object is rooted. Here is a partial listing from the first Item object. For this object, the root is the Application 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.
        NativeMethods+WndProc)->
    0139ec94(System.Windows.Forms.Control+ControlNativeWindow)->
    0139ebc4(System.Windows.Forms.CheckBox)->
    0139d6f8(Store.Transaction)->
    013a03b8(Store.Item)
  6. As shown with the dumpheap command, transaction objects are 20 bytes. You might be curious about what those 20 bytes contain. To answer that question, look at 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];
    }

Listing the source code is easy if the source code is available. But what if the source code is not available (which would not be unusual for a production application)? The !dumpclass command displays a class, including members. It uses the EEClass address, which is provided with the !name2ee command. The !dumpclass command provides not only type information but also the state of static members. Static members belong to the class. In the following listing, the class is listed. It also shows that the static nextPropId property is 3 at the moment:

0:004> !dumpclass 00db21c4
Class Name: Store.Item
mdToken: 02000002 (C:storeStore.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 a 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 communicate with other objects are kept close together in memory. This decreases page faults, which are costly, and decreases the amount of physical memory required at any time. Reducing page faults makes your application perform faster.

Garbage collection in .NET often is described as nondeterministic, which means that garbage collection is not called on demand. Garbage collection occurs automatically when memory allocation exceeds the memory reserved for a particular generation. When an application starts, Generation 0 is available for allocation. Eventually, the memory available to Generation 0 is exceeded, which triggers garbage collection on Generation 0 alone. This is more efficient because only a portion of the managed heap is experiencing 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 Generations 0 and 1 are replete with objects. This time, garbage collection is performed on Generations 0 and 1. At that time, Generation 0 and Generation 1 objects are promoted to Generation 1 and Generation 2, respectively. By design, the older and larger objects tend to migrate toward the higher generations, whereas younger and smaller objects are found in lower generations.

Memory on the managed heap is allocated top-down. In Win32 applications, zero is at the bottom of the memory. Higher addresses are at the top of the memory space. New objects in managed code are allocated at higher addresses on the managed heap. The lower generations are at higher memory addresses. 256 kilobytes (KB), 2 megabytes (MB), and 10 MB are reserved for Generation 0, Generation 1, and Generation 2, respectively. These thresholds can 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 (that is, objects greater than 85 KB). Instead of promoting these objects from one generation to another, which is costly, large objects are placed on the large object heap immediately at allocation.

Generations Example

This time, the Store application has both an Add Transactions button and an Add Large Transactions button. The Add Large Transactions button adds large transactions (not surprisingly), 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 therefore qualifies as a large object. The objective of this example is to determine the generation of each Item, LargeItem, and largeStuff instance.

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

  2. Start WinDbg and attach to the Store application. Load SOS.

  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 Item 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? The answer to this question will be provided shortly.

  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. One way to locate them is to use 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. Four instances are shown. The first column of the command is the method table address. Dump the largeStuff instances using that address with 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 ranges 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 16-8 shows where each object is allocated on the managed heap. None of the instances resides in Generation 0.

Table 16-8. Location on managed heap of Item, LargeItem, and largeStuff instances

Item

Address

No objects in Generation 0

N/A

Generation 0 starts

0x013d2488

LargeItem4

0x013ca900

Item6

0x013c1a4c

LargeItem3

0x013be020

Item5

0x013b513c

Generation 1 starts

0x013af93c

LargeItem2

0x013ae5a4

LargeItem1

0x013ab03c

LargeItem2

0x013ae5a4

Item3

0x013aadd8

Item2

0x013aabb0

Item1

0x013a8bd0

Generation 2 starts

0x01391000

largeStuff4

0x02f08918

largeStuff3

0x02b37fe8

largeStuff2

0x027676c8

largeStuff1

0x02396da8

largeStuff2

0x027676c8

Large object heap

0x02391000

It is time to resolve the earlier question. Remember that there were six instances of the Item type. However, three regular transactions were created. Actually, this result is correct because of nondeterministic collection. In the Store application, every transaction starts as an Item object. LargeItem objects are created within the 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 unneeded items will be removed from memory. For demonstration purposes, there is a version of the Store application included that can force garbage collection. It has a Collect Memory button that calls GC.Collect and removes the extra Item instances and other unused objects. This version of the application is found in the gccollect subfolder. When using this Store application, display the Item instances (with the dumpheap command) both before and after clicking Collect Memory to confirm that GC.Collect is reclaiming the extra items. In general, however, calling GC.Collect is not recommended because it is costly.

Finalization

Up to now we have ignored the finalization process. But it must be considered because it plays a vital role in garbage collection. Finalization affects the performance and effectiveness of garbage collection. In C#, finalization is linked to class destructors. For C++ programmers, .NET presents a different model for destructors.

Object.Finalize is the universal destructor in .NET. In C#, Finalize calls the class destructor. Destructors are called deterministically in C++. However, in C# the CLR calls destructors nondeterministically during garbage collection. You cannot 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. Other managed objects might have been removed from memory already during garbage collection.

For deterministic garbage collection, implement the IDisposable interface and the Dispose method. You then can call the Dispose method directly.

Destructors add processing overhead to an object. The overhead is incurred even before the object is collected. The GC adds references for objects with destructors to the Finalization queue when the object is created. (This does not occur for objects without destructors.) 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 collectable objects with destructors 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 deleted from memory at the next garbage collection.

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

Reliability and Performance Monitor

In Windows Vista, the Reliability and Performance Monitor is located in the Administrative Tools folder on the Control Panel. The Performance Monitor has several counters that are helpful when debugging managed applications. Table 16-9 itemizes some of the more useful memory-related counters. These counters are found in the .NET CLR Memory category.

Table 16-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

The total bytes allocated for Generation 0, 1, 2, and the large object heap.

# Induced GC

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

# Of Pinned Objects

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

# Of Sync Blocks In Use

A count of syncblock entries. (Syncblock entries are discussed in the section "Threads," later in this chapter.)

# 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 bytes of virtual memory committed by the GC.

# Total Reserved Bytes

The total bytes of virtual memory reserved by the GC.

Gen 0 Heap Size

The total bytes allocated for Generation 0.

Gen 1 Heap Size

The total bytes allocated for Generation 1.

Gen 2 Heap Size

The total bytes allocated for Generation 2.

Large Object Heap Size

The total bytes allocated for the large object heap.

% Time In GC

The percentage of application execution 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 objects that survived garbage collection and are waiting for destructors to be called.

Gen 0 Promoted Bytes/Sec

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

Gen 1 Promoted Bytes/Sec

The number of bytes per second promoted from Generation 1 to 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.97.126