Chapter 13. Managing How the CLR Uses Memory

The hosting of the CLR within Microsoft SQL Server 2005 has required the CLR to take a new approach to obtaining the resources it historically has obtained directly from the operating system. Specifically, a new level of abstraction has been added to the Microsoft .NET Framework 2.0 CLR that enables a host to supply the CLR with resources such as memory, threads, and synchronization primitives. This abstraction layer is made available to hosts in the form of new managers in the CLR hosting API. If the CLR is hosted and the host has indicated the desire to supply the CLR with a specific type of resource by implementing the appropriate manager, the CLR will call the host to obtain the resource instead of getting it directly from the operating system.

The CLR hosting API includes managers that enable the host to supply the CLR with the basic primitives it needs to allocate and free memory, create and destroy executable tasks, queue work items to a thread pool, create synchronization primitives, and so on. The CLR’s new approach to obtaining the basic resources it needs to run programs is very powerful because it enables the CLR to be integrated into environments with very specific requirements for resource management. In this chapter, I discuss the APIs a host can use to supply the CLR with the primitive functions it needs to manage memory. In Chapter 14, I discuss how a host can use the CLR hosting API to integrate the CLR into environments with specific threading and concurrency requirements.

To gain a better understanding of the scenarios in which a host might want to provide the hosting managers that enable it to replace the basic primitives the CLR uses to create and manage resources, consider SQL Server requirements. In many ways, SQL Server behaves like an operating system on machines on which it runs. For example, SQL Server closely manages all the memory it uses, it can be run in a mode in which it manually schedules executable tasks on fibers instead of leaving all scheduling up to the operating system, and so on. These custom subsystems are highly tuned to provide the highest overall throughput and performance in the most critical SQL Server scenarios. The primary challenge in integrating the CLR into SQL Server 2005 was getting two systems that were designed independently to cooperate on issues related to resource management in such a way that the overall solution doesn’t adversely affect SQL Server scalability and performance requirements.

Memory management provides a good example illustrating the changes required by the CLR to ensure a seamless integration with SQL Server 2005. SQL Server operates within a configurable amount of memory. In many cases, SQL Server is the only application running on a given server and is therefore configured to use most all of the machine’s physical memory. A key design goal of SQL Server is never to allocate more memory than it is configured to use.

Although it is certainly possible, given the operating system’s virtual memory system, allocating more memory than is physically available causes memory pages to be swapped out to disk only to be reloaded later when they are needed. Paging in the virtual memory system introduces a performance cost that is unacceptable in many SQL Server scenarios. In other words, if the request to execute a particular query would cause the operating system to page, SQL Server is designed to fail the request rather than incur the performance cost associated with demand paging. SQL Server tracks all memory allocations made in the process to ensure it never exceeds the amount of memory it is configured to use. If a request to allocate memory would cause SQL to exceed its configured limit, that request is denied. This approach works great as long as SQL Server is the only entity in the process that is allocating memory.

However, the introduction of the CLR into the process means that there is an additional entity making requests for memory. Clearly, for SQL Server to be able to track all memory allocated in the process accurately, all of the CLR’s requests for memory must go through SQL Server instead of directly to the operating system. This is where the CLR hosting APIs come into play. By implementing a memory manager using the CLR hosting API, SQL Server can intercept all memory requests and handle them as it sees fit. In this way, SQL Server is able to track all memory requests in the process accurately regardless of whether they are initiated by SQL Server or by the CLR. In addition, if a memory request initiated by the CLR would cause SQL Server to exceed its configured limit, SQL Server can return a failure to the CLR indicating that no more memory is available. In this way, SQL Server can regulate the amount of memory the CLR can use. The redirection of the CLR’s memory requests through the hosting API and into SQL Server is shown in Figure 13-1.

The CLR redirects all requests for memory through the hosting API while running inside SQL Server.

Figure 13-1. The CLR redirects all requests for memory through the hosting API while running inside SQL Server.

The CLR hosting API exposes two managers that hosts can use to configure how the CLR uses memory in the process. The first of these managers enables a host to supply the CLR with basic memory allocation primitives, whereas the second hosting manager provides functions a host can use to configure the CLR’s garbage collection. I cover both of these managers in detail throughout the chapter.

Integrating the CLR with Custom Memory Managers

If you need to constrain or otherwise customize how the CLR uses memory, use the CLR hosting API to implement a memory manager. Implementing a memory manager enables you to customize the following aspects of how memory is managed in your process:

  • Virtual memory management

  • Heap management

  • File mapping functions

  • Reporting the current memory load to the CLR

These capabilities are provided by the three interfaces that make up a memory manager: IHostMemoryManager, IHostMalloc, and ICLRMemoryNotificationCallback. As the primary interface in the hosting manager, IHostMemoryManager is the interface the CLR asks the host for during initialization. The CLR determines whether a host implements the memory manager by passing the IID for IHostMemoryManager to the GetHostManager method of the host’s implementation of IHostControl. (Refer to Chapter 2 for a complete description of how the CLR determines which managers a particular host implements.)

In the next several sections, I provide the details on how to use the capabilities offered by the three interfaces that comprise the memory manager.

Virtual Memory Management

The CLR relies on the features provided by Microsoft Win32 virtual memory management functions to allocate the memory it needs for its garbage collection heaps and to store other internal CLR data structures. If you implement a memory manager in your host, you must provide the CLR with a set of virtual memory management functions that map to those provided by Win32. As you can see in Table 13-1, IHostMemoryManager has a set of methods whose names match those of the Win32 virtual memory functions. When you implement a memory manager, the CLR will use the virtual memory methods provided by your implementation of IHostMemoryManager instead of calling those provided by Win32.

Table 13-1. The Methods on the IHostMemoryManager Interface

Method

Description

VirtualAlloc

Equivalent to the VirtualAlloc API in Win32.

VirtualFree

Equivalent to the VirtualFree API in Win32.

VirtualQuery

Equivalent to the VirtualQuery API in Win32.

VirtualProtect

Equivalent to the VirtualProtect API in Win32.

CreateMalloc

Returns an interface of type IHostMalloc that the CLR will use to access methods needed to manage memory in heaps. I describe this method in more detail in the "Heap Management" section later in this chapter.

GetMemoryLoad

The CLR calls this method to get an indication of the current memory load on the system. I describe this method in more detail later in this chapter in the section called "Reporting Memory Status to the CLR."

RegisterMemoryNotificationCallback

The CLR calls this method to register a callback you can use to notify the CLR of the current memory load on the system. I describe this method in more detail later in this chapter in the section called "Reporting Memory Status to the CLR."

NeedsVirtualAddressSpace

The CLR calls NeedsVirtualAddressSpace to determine whether space is available to map a file on disk into memory. I describe this later in the "File Mapping" section of this chapter.

AcquiredVirtualAddressSpace

The CLR reports the memory it has allocated to map disk files into memory by calling AcquiredVirtualAddressSpace. I describe this later in the "File Mapping" section of this chapter.

ReleasedVirtualAddressSpace

After the CLR has freed the memory it needed to map files into memory, it notifies the host by calling ReleasedVirtualAddressSpace.

Most of the parameters to the VirtualAlloc, VirtualFree, VirtualQuery, and VirtualProtect methods on IHostMemoryManager map directly to those provided by the Win32 APIs of the same name. The one notable exception is the eCriticalLevel parameter to IHostMemoryManager::VirtualAlloc, which is unique to the virtual memory management functions provided by the CLR hosting API. Here’s the definition of the VirtualAlloc method on IHostMemoryManager from mscoree.idl:

interface IHostMemoryManager : IUnknown
{
   // Other methods omitted...

    HRESULT VirtualAlloc([in] void*       pAddress,
                         [in] SIZE_T      dwSize,
                         [in] DWORD       flAllocationType,
                         [in] DWORD       flProtect,
                         [in] EMemoryCriticalLevel eCriticalLevel,
                         [out] void**     ppMem);
}

Hosts are free to deny the CLR’s request for more memory by returning the E_OUTOFMEMORY HRESULT from their implementation of VirtualAlloc. However, depending on how critical the CLR’s need for more memory is when it calls VirtualAlloc, it might not be able to complete certain operations if the request for more memory is denied. The consequences of denying a request for more memory are communicated to the host using eCriticalLevel. Typically, the failure to allocate memory causes the CLR to abort the specific task[1] it is executing at the time. In more extreme cases, the failure to obtain more memory can cause the CLR to unload the current application domain or even terminate the entire process. Each time the CLR calls VirtualAlloc, it passes in a value from the EMemoryCriticalLevel indicating whether a failure to obtain the requested memory will cause the task, application domain, or process to be terminated. Here’s the definition of EMemoryCriticalLevel from mscoree.idl:

typedef enum
{
   eTaskCritical = 0,
   eAppDomainCritical = 1,
   eProcessCritical = 2
} EMemoryCriticalLevel;

Given that a host can use a memory manager to supply the CLR with implementations of VirtualAlloc and VirtualFree, it’s relatively easy to see how SQL Server uses the CLR hosting API to make sure that it never exceeds the amount of memory it is configured to use. SQL Server implementation of IHostMemoryManager records both the sizes of all memory allocations that are made through VirtualAlloc and the amount of memory freed by each call to VirtualFree. The difference between the two values is the amount of virtual memory the CLR is using at any one time. This total, combined with the virtual memory allocated by SQL Server, is always kept under the amount that SQL Server is configured to use.

Throughout this section, I haven’t said anything about how a host’s implementation of the virtual memory management methods on IHostMemoryManager should behave, other than to allocate (or deny) and free the memory that is requested by the CLR. With the exception of these basic requirements, the host is free to implement these methods any way it sees fit. This freedom is a very powerful aspect of the abstraction provided by the CLR hosting API. In some cases, a host might choose simply to delegate the calls to the virtual memory methods on IHostMemoryManager to their Win32 equivalents after doing any bookkeeping that is necessary. A host’s implementation of these methods need not do that, however. If a host has specific requirements around the timing of memory allocations, the locations from which the memory comes, and so on, it can use the abstraction provided by the hosting APIs to hide these details from the CLR.

The management of virtual memory is only one part of the overall picture, however. In the next section, you’ll see how a host can use a memory manager to supply the CLR with the primitives it uses to manage memory allocated in heaps.

Heap Management

In addition to the virtual-memory APIs described in the previous section, the CLR also relies on a set of functions that enable it to manage memory in heaps. A host provides the CLR with these functions through the IHostMalloc interface. As you can see from Table 13-2, IHostMalloc contains methods that correspond to heap management APIs provided by Win32.

Table 13-2. The Methods on the IHostMalloc Interface

Method

Description

Alloc

Allocates memory from the heap

DebugAlloc

The debug version of Alloc

Free

Releases memory previously allocated with Alloc or DebugAlloc

The CLR obtains the IHostMalloc interface from the host by calling the CreateMalloc method on IHostMemoryManager. CreateMalloc takes a set of flags that identify the characteristics needed in the heap that is returned as shown in the following definition from mscoree.idl:

interface IHostMemoryManager : IUnknown
{
   HRESULT CreateMalloc([in] DWORD dwMallocType,
                        [out] IHostMalloc **ppMalloc);

   // Other methods omitted...
}

The valid flags to CreateMalloc are represented by the MALLOC_TYPE enumeration:

typedef enum
{
   MALLOC_THREADSAFE = 0x1,
   MALLOC_EXECUTABLE = 0x2,
} MALLOC_TYPE;

The MALLOC_THREADSAFE flag indicates that the CLR must be able to safely allocate and free data in the heap from multiple threads simultaneously. The current version of the CLR always sets this value. The CLR sets the MALLOC_EXECUTABLE flag when it intends to store executable code in the heap. It sets this, for example, when it dynamically creates and stores the code it uses as part of the COM Interoperability layer. The MALLOC_EXECUTABLE flag exists so the host and the CLR can properly use the No Execute (NX) feature available on some processors today. Essentially, NX enables memory pages to be marked with a bit that prevents executable code from being stored and run from the page. By marking pages in this way, the potential for security vulnerabilities is reduced when a page is not explicitly intended to contain executable code. Specifically, NX helps mitigate the vulnerability whereby a malicious party writes code into random locations in memory and causes it to be executed.

In the same way that all requests for virtual memory come through IHostMemoryManager, all requests to allocate memory from a heap come through IHostMalloc. If you are implementing a memory manager for the purposes of restricting the amount of memory the CLR can use, remember to account for the memory allocated through IHostMalloc when determining how much memory the CLR has requested.

File Mapping

The CLR uses the Win32 memory-mapped file APIs when loading and executing assemblies. Because the process of mapping a file requires address space, the CLR must keep the host informed of all memory allocated while mapping files. The methods used to communicate information about file mappings to the host are the NeedsVirtualAddressSpace, AcquiredVirtual-AddressSpace, and ReleasedVirtualAddressSpace methods on IHostMemoryManager. Here are the definitions of those methods from mscoree.idl:

interface IHostMemoryManager : IUnknown
{
   HRESULT NeedsVirtualAddressSpace(
      [in] LPVOID startAddress,
      [in] SIZE_T size
      );

   HRESULT AcquiredVirtualAddressSpace(
      [in] LPVOID startAddress,
      [in] SIZE_T size
      );

   HRESULT ReleasedVirtualAddressSpace(
      [in] LPVOID startAddress
      );
}

The CLR maps files into memory using the Win32 MapViewOfFile API. All address space acquired by calling MapViewOfFile is reported to the host by calling AcquiredVirtualAddressSpace. If the host is using a memory manager to keep track of the amount of address space used by the CLR, it must include the address space reported through AcquiredVirtualAddressSpace in its totals. If the CLR’s call to MapViewOfFile fails because of low address space conditions, the CLR tells the host that it needs additional address space by calling NeedsVirtualAddressSpace and passing in the start address and size of the address space it needs. If the host is able to make the address space available, it returns the S_OK HRESULT from NeedsVirtualAddressSpace. The CLR will then try to call MapViewOfFile again, assuming that the required address space is now available. After the CLR unmaps a file using the Win32 UnmapViewOfFile API, it notifies the host that the virtual address space used by the file mapping is now free by calling ReleasedVirtualAddressSpace.

Reporting Memory Status to the CLR

One of the heuristics the CLR uses to determine when to perform a garbage collection is the amount of memory pressure currently on the system. If memory pressure is high (signifying that very little memory is available), the CLR will do a garbage collection and return all the memory it can to the system. The CLR uses two Win32 APIs to determine the current memory load: GlobalMemoryStatus and the memory resource notification created with CreateMemoryResourceNotification. Hosts that implement a memory manager can provide replacements for these APIs so that a host’s own impression of the current memory load can be used to influence when garbage collections occur.

The GetMemoryLoad Method

The equivalent of the Win32 GlobalMemoryStatus API is the GetMemoryLoad method on IHostMemoryManager. The CLR will call GetMemoryLoad periodically to determine the memory load on the system from the host’s perspective. The host returns two values from GetMemoryLoad, as shown in the following definition from mscoree.idl:

interface IHostMemoryManager : IUnknown
{
    // Other methods omitted...
    HRESULT GetMemoryLoad([out] DWORD* pMemoryLoad,
                          [out] SIZE_T *pAvailableBytes);
}

The first parameter, pMemoryLoad, is the percentage of physical memory that is currently in use. This parameter is equivalent to the dwMemoryLoad field of the MEMORYSTATUS structure returned from GlobalMemoryStatus. The pAvailableBytes parameter is the number of bytes that are currently available for the CLR to use.

The exact behavior of the CLR in response to the values returned from GetMemoryLoad isn’t defined and is likely to change between releases. That is, returning specific values doesn’t guarantee that a specific amount of memory will be freed or even that a garbage collection will be done immediately. All that is guaranteed is that the CLR considers the values returned from GetMemoryLoad when determining the timing of the next garbage collection.

The ICLRMemoryNotificationCallback Interface

The CLR calls the GetMemoryLoad method on IHostMemoryManager when it wants to determine the current memory load on the system. As a host, you have no control over when GetMemoryLoad is called. However, you can be more proactive about notifiying the CLR of the current memory status by calling the methods on an interface provided by the CLR called ICLRMemoryNotificationCallback.

The process of reporting memory status through this callback is as follows. After the CLR obtains your memory manager by calling IHostControl::GetHostManager, it calls the RegisterMemoryNotificationCallback method on IHostMemoryManager, passing in an interface pointer of type ICLRMemoryNotificationCallback. The definition of RegisterMemoryNotificationCallback is shown here:

interface IHostMemoryManager : IUnknown
{
    // Other methods omitted...
       HRESULT RegisterMemoryNotificationCallback(
           [in] ICLRMemoryNotificationCallback * pCallback);
}

A host’s implementation of RegisterMemoryNotificationCallback should save a copy of the ICLRMemoryNotificationCallback interface pointer it is given by the CLR. At any time, the host can call back through ICLRMemoryNotificationCallback to report memory status to the CLR. ICLRMemoryNotificationCallback has a single method called OnMemoryNotification as shown in the following definition from mscoree.idl:

interface ICLRMemoryNotificationCallback : IUnknown
{
    HRESULT OnMemoryNotification([in] EMemoryAvailable eMemoryAvailable);
}

Memory status is reported to the CLR by passing a value from the EMemoryAvailable enumeration:

typedef enum
{
    eMemoryAvailableLow = 1,
    eMemoryAvailableNeutral = 2,
    eMemoryAvailableHigh = 3
} EMemoryAvailable;

The most useful value from EMemoryAvailable is the eMemoryAvailableLow. When this value is passed, the CLR will perform a garbage collection in an attempt to make more storage available to the system. The current version of the CLR doesn’t take any action at all when it receives either eMemoryAvailableNeutral or eMemoryAvailableHigh from the host.

Configuring the CLR Garbage Collector

The CLR hosting APIs offer two more interfaces that hosts can use to monitor and configure how the CLR uses memory. These two interfaces, ICLRGCManager and IHostGCManager, comprise the hosting interface’s garbage collection manager. The ICLRGCManager interface enables a host to initiate garbage collections, gather various statistics related to collections, and partition the garbage collector’s heap for optimal performance. Hosts can receive notifications about the timing of garbage collections by providing the CLR with an interface of type IHostGCManager. I discuss the role of IHostGCManager in more detail later in the chapter in the section entitled "Receiving Notifications Through the IHostGCManager Interface."

CLR hosts obtain an interface pointer of type ICLRGCManager through the standard mechanism used to obtain hosting interfaces from the CLR—that is, by calling the GetCLRManager method on the CLR’s implementation of ICLRControl, as shown in the following sample main program. (Refer to Chapter 2 for a detailed discussion of how both the host and the CLR exchange pointers to the various interfaces in the hosting API.)

int wmain(int argc, wchar_t* argv[])
{
   HRESULT hr = S_OK;

   // Start .NET Framework version 2.0 of the CLR.
   ICLRRuntimeHost *pCLR = NULL;
   hr = CorBindToRuntimeEx(
      L"v2.0.41013",
      L"wks",
      STARTUP_CONCURRENT_GC,
      CLSID_CLRRuntimeHost,
      IID_ICLRRuntimeHost,
      (PVOID*) &pCLR);

   assert(SUCCEEDED(hr));

   // Get the CLRControl object. Use this to get the pointer of
   // type ICLRGCManager.
   ICLRControl *pCLRControl = NULL;
   hr = pCLR->GetCLRControl(&pCLRControl);
   assert(SUCCEEDED(hr));

   // Get a pointer to an ICLRGCManager.
   ICLRGCManager *pCLRGCManager = NULL;
   hr = pCLRControl->GetCLRManager(IID_ICLRGCManager,
                                   (void **)&pCLRGCManager);
   assert(SUCCEEDED(hr));

   // The ICLRGCManager pointer is now ready to use...

   // Remember to release it.
   pCLRGCManager->Release();

   // The rest of the host's code is omitted...
}

Once the host has a pointer of type ICLRGCManager, it can use the methods on that interface to customize various aspects of how the garbage collector works. Table 13-3 provides an overview of the methods on ICLRGCManager.

Table 13-3. The Methods on the ICLRGCManager Interface

Method

Description

Collect

Enables a host to initiate a garbage collection

GetStats

Returns various statistics about the garbage collections that have occurred so far in the process

SetGCStartupLimits

Enables a host to partition the garbage collection heap to optimize overall performance for its specific scenario

The next several sections describe how to use the methods in Table 13-3.

Partitioning the Garbage Collector’s Heap

The CLR garbage collector uses the notion of generations to optimize the collector based on the expected lifetime of managed objects in the heap. A complete description of how the garbage collector uses generations is described in many other texts, so I won’t repeat it here. If you’re looking for a great reference on the CLR’s garbage collector, refer to Chapter 19 in Applied Microsoft .NET Framework Programming by Jeffery Richter (Microsoft Press, 2002).

The SetGCStartupLimits on ICLRGCManager can be used to specify how much of the garbage collection heap is to be used for generations 0 and 1. The garbage collection heap is divided into segments. The managed objects in generations 0 and 1 are stored in the same segment. The objects in generation 2 are sometimes stored in the same segment as generations 0 and 1, but not always. The decision about where the objects in generation 2 and the objects in the large object heap live is a CLR implementation detail that can change over time. The relationship between segments and generations is shown in Figure 13-2.

The garbage collection heap is partitioned into segments.

Figure 13-2. The garbage collection heap is partitioned into segments.

SetGCStartupLimits enables you to supply two values that control how the garbage collection heap is partitioned, as shown in the following definition from mscoree.idl:

interface ICLRGCManager : IUnknown
{
    // Other methods omitted...
    HRESULT SetGCStartupLimits([in] DWORD SegmentSize,
                               [in] DWORD MaxGen0Size);
}

The SegmentSize parameter to SetGCStartupLimits controls the size of the segments in the garbage collection heap. The value you supply to this method must be a multiple of 1 MB and at least 4 MB. The MaxGen0Size specifies the size of the space used to store objects in generation 0. MaxGen0Size must be at least 64 KB. Both SegmentSize and MaxGen0Size are specified in bytes. Given values for SegmentSize and MaxGen0Size, the CLR computes the amount of space to use for generation 1 as shown in Figure 13-2. Both values you supplied through SetGCStartupLimits can be set only once—subsequent calls are ignored. The following sample call to SetGCStartupLimits sets the segment size to 8 MB and the maximum size of generation 0 to 128 KB:

hr = pCLRGCManager->SetGCStartupLimits(8*1024*1024,128*1024);

Now that you know how to partition the garbage collection heap using SetGCStartupLimits, take a look at how you might use the statistics returned from ICLRGCManager::GetStats to determine which values for SegmentSize and MaxGen0Size might work best for your application.

Gathering Garbage Collection Statistics

The ability to partition the garbage collection heap isn’t of much use if you don’t know which values for SegmentSize and MaxGen0Size make sense in your application. Settling on the right values will likely take several iterations, but the GetStats method on ICLRGCManager can help you get started. GetStats returns a structure that contains various statistics about how the garbage collector is performing in your process. By looking at the values returned from GetStats, you can start to establish patterns that can help you optimize the performance of the garbage collector by adjusting how the heap is partitioned using SetGCStartupLimits.

GetStats returns a structure of type COR_GC_STATS as shown in the following definition from mscoree.idl:

interface ICLRGCManager : IUnknown
{
    HRESULT GetStats([in][out] COR_GC_STATS *pStats);
}

The COR_GC_STATS structure contains fields that report both the number of collections that have occurred and the current status of the memory used by the garbage collector. COR_GC_STATS is defined in gchost.idl in the .NET Framework SDK:

typedef struct _COR_GC_STATS
{
    ULONG           Flags;

    SIZE_T           ExplicitGCCount;
    SIZE_T           GenCollectionsTaken[3];

    SIZE_T           CommittedKBytes;
    SIZE_T           ReservedKBytes;
    SIZE_T           Gen0HeapSizeKBytes;
    SIZE_T           Gen1HeapSizeKBytes;
    SIZE_T           Gen2HeapSizeKBytes;
    SIZE_T           LargeObjectHeapSizeKBytes;
    SIZE_T           KBytesPromotedFromGen0;
    SIZE_T           KBytesPromotedFromGen1;
} COR_GC_STATS;

Notice from the definition of GetStats that the pStats parameter is marked as both an in and an out parameter. When calling GetStats, you must first populate the Flags fields of COR_GC_STATS to indicate which of the statistics you’d like populated. Based on the flags you set, the CLR fills in the appropriate fields of the structure that you passed in. The valid values for the Flags field are given by the COR_GC_STAT_TYPES enumeration from gchost.idl:

typedef enum
{
    COR_GC_COUNTS      = 0x00000001,
    COR_GC_MEMORYUSAGE = 0x00000002,
} COR_GC_STAT_TYPES;

If COR_GC_COUNTS is added to the Flags field of COR_GC_STATS, the CLR populates the fields of COR_GC_STATS that describe the number of collections that have occurred so far in the process. These fields are ExplicitGCCount and GenCollectionsTaken. ExplicitGCCount indicates the number of times that a garbage collection has been explicitly initiated either through a call to ICLRGCManager::Collect or through the Collect method on the System.GC class in the .NET Framework class libraries. The GenCollectionsTaken array describes the number of collections that have occurred per generation. Element 0 of GenCollectionsTaken contains the number of collections done in generation 0, element 1 contains the number of collections done in generation 1, and so on.

Setting COR_GC_MEMORYUSAGE in the Flags field of COR_GC_STATS causes the CLR to return the values that provide insight into how the garbage collector is using memory in the process. The fields returned when COR_GC_MEMORYUSAGE is set are as follows:

  • CommittedKBytes

  • ReservedKBytes

  • Gen0HeapSizeKBytes

  • Gen1HeapSizeKBytes

  • Gen2HeapSizeKBytes

  • LargeObjectHeapSizeKBytes

  • KBytesPromotedFromGen0

  • KBytesPromotedFromGen1

These fields describe the total amount of memory that has been committed and reserved by the garbage collector, the number of bytes currently used to store the objects in each generation, and the number of bytes promoted from generation 0 to generation 1 and from generation 1 to generation 2.

The following sample call sets both COR_GC_COUNTS and COR_GC_MEMORYUSAGE to return all of the statistics available from GetStats:

COR_GC_STATS stats;
stats.Flags = COR_GC_COUNTS | COR_GC_MEMORYUSAGE;

hr = pCLRGCManager->GetStats(&stats);
// The stats structure now contains values for the full set
// of garbage collection statistics.

Using the statistics returned from GetStats to tune the CLR garbage collector requires several iterations and an extensive amount of testing. It’s very easy to hurt performance instead of help it if you’re not careful. If you see that an excessive number of generation 0 collections are happening in your particular scenario, you might try adjusting the amount of space the CLR is using to store generation 0 objects. However, be sure you follow up with enough benchmarking to ensure you aren’t inadvertently making matters worse.

Initiating Garbage Collections

The CLR uses several heuristics to determine when to initiate a garbage collection. For example, a collection is done when generation 0 is full. These heuristics work great for the vast majority of application scenarios, and the general guidance is to leave it up to the CLR to determine the optimal time to do a collection. That said, available APIs enable you to force a garbage collection to happen. The CLR hosting API offers the ability to initiate a garbage collection by calling the Collect method on ICLRGCManager. Collect takes a single parameter that identifies the generation you’d like collected, as shown in the following definition from mscoree.idl:

interface ICLRGCManager : IUnknown
{
    HRESULT Collect([in] LONG Generation);
    // Other methods omitted...
}

To force a collection for a particular generation, simply pass the number of that generation (0, 1, or 2) as the Generation parameter. You can force all generations to be collected by passing –1.

As with all techniques available to configure the CLR garbage collector, use the ability to initiate collections programmatically with care. Just the act of preparing for a garbage collection can be an expensive operation. To prepare, the CLR must bring all threads to a known safe state and ensure that several internal data structures cannot be modified while the collection is performed. Calling Collect too often can easily degrade performance by causing the CLR unnecessarily to bring itself to a state in which it’s safe to perform a collection. Again, be sure to test thoroughly to make sure you’re not hurting performance when you intend to make it better.

Note

Note

The CLR hosting APIs from .NET Framework 1.0 and .NET Framework 1.1 include an interface in gchost.idl called IGCHost. This interface has many of the same capabilities that ICLRGCManager now has. IGCHost is now deprecated and should not be used going forward. Always use ICLRGCManager instead.

Receiving Notifications Through the IHostGCManager Interface

In Chapter 14, you’ll see that the CLR hosting APIs provide a set of interfaces that enable a host to integrate the CLR with custom task-scheduling schemes. To schedule tasks most efficiently, a host must know when the CLR suspends a thread either to do a garbage collection or for other activities. A host can use the knowledge of when a thread is about to be suspended to avoid scheduling any tasks on that thread until the CLR is ready to let the thread run again.

The CLR notifies the host when a thread is about to be suspended (and when it resumes) by calling methods on the host’s implementation of IHostGCManager. The methods on IHostGCManager are shown in Table 13-4.

Table 13-4. The Methods on the IHostGCManager Interface

Method

Description

ThreadIsBlockingForSuspension

Notifies the host that the thread making this call is about to block for a garbage collection or other activity. At this point, the host should not schedule any managed code to run on the thread.

SuspensionStarting

Notifies the host that a thread suspension is beginning.

SuspensionEnding

Notifies the host that a thread suspension is ending SuspensionEnding includes a parameter that indicates for which generation garbage was collected while the thread was suspended.

Hosts provide the CLR with an implementation of IHostGCManager using the standard technique employed for all of the hosting interfaces implemented by the host. Specifically, a host must do the following:

  1. Define a class that derives from IHostGCManager.

  2. Return an instance of that class when the CLR calls the host’s implementation of IHostControl::GetHostManager, passing in the interface identifier for IHostGCManager (IID_IHostGCManager).

I provide many more details on how to integrate the CLR with a custom scheduler in Chapter 14.

Summary

The interfaces that make up the memory manager enable a host to control closely how the CLR uses memory in a process. These interfaces effectively form an abstraction layer between the CLR and the operating system by enabling the host to provide the basic memory management primitives for which the CLR typically relies on the operating system. By implementing a memory manager, a host can control basic functions such as the use of virtual memory and the management of heaps. SQL Server 2005 uses a memory manager to ensure that memory allocated by the CLR doesn’t cause SQL Server to exceed the amount of memory it is configured to use.

Hosts can also affect the CLR’s use of memory using the interfaces that constitute the garbage collection manager. The ICLRGCManager interface enables the host to optimize how the garbage collector functions by partitioning the garbage collection heap in ways that best fit the memory usage patterns of the host. Configuring the garbage collector requires extensive iteration and testing. It’s surprisingly easy to hinder performance inadvertently if you haven’t done sufficient benchmarking.



[1] In this context, the term task refers to either an operating system thread or a host-provided fiber as described in Chapter 14.

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

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