Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Managed Heap Basics

Every program uses resources of one sort or another, be they files, memory buffers, screen space, network connections, database resources, and so on. In fact, in an object-oriented environment, every type identifies some resource available for a program’s use. To use any of these resources requires memory to be allocated to represent the type. The following steps are required to access a resource:

1.Allocate memory for the type that represents the resource (usually accomplished by using C#’s new operator).

2.Initialize the memory to set the initial state of the resource and to make the resource usable. The type’s instance constructor is responsible for setting this initial state.

3.Use the resource by accessing the type’s members (repeating as necessary).

4.Tear down the state of a resource to clean up.

5.Free the memory. The garbage collector is solely responsible for this step.


This seemingly simple paradigm has been one of the major sources of problems for programmers that must manually manage their memory; for example, native C++ developers. Programmers respon- sible for managing their own memory routinely forget to free memory, which causes a memory leak. In addition, these programmers frequently use memory after having released it, causing their program to experience memory corruption resulting in bugs and security holes. Furthermore, these two bugs are worse than most others because you can’t predict the consequences or the timing of them. For other bugs, when you see your application misbehaving, you just fix the line of code that is not working.

As long as you are writing verifiably type-safe code (avoiding C#’s unsafe keyword), then it is im- possible for your application to experience memory corruption. It is still possible for your application to leak memory but it is not the default behavior. Memory leaks typically occur because your applica- tion is storing objects in a collection and never removes objects when they are no longer needed.

To simplify things even more, most types that developers use quite regularly do not require Step 4 (tear down the state of the resource to clean up). And so, the managed heap, in addition to abolish- ing the bugs I mentioned, also provides developers with a simple programming model: allocate and initialize a resource and use it as desired. For most types, there is no need to clean up the resource and the garbage collector will free the memory.

When consuming instances of types that require special cleanup, the programming model remains as simple as I’ve just described. However, sometimes, you want to clean up a resource as soon as possible, rather than waiting for a GC to kick in. In these classes, you can call one additional method (called Dispose) in order to clean up the resource on your schedule. On the other hand, implement- ing a type that requires special cleanup is quite involved. I describe the details of all this in the “Work- ing with Types Requiring Special Cleanup” section later in this chapter. Typically, types that require special cleanup are those that wrap native resources like files, sockets, or database connections.



 

Allocating Resources from the Managed Heap

The CLR requires that all objects be allocated from the managed heap. When a process is initialized, the CLR allocates a region of address space for the managed heap. The CLR also maintains a pointer, which I’ll call NextObjPtr. This pointer indicates where the next object is to be allocated within the heap. Initially, NextObjPtr is set to the base address of the address space region.

As region fills with non-garbage objects, the CLR allocates more regions and continues to do this until the whole process’s address space is full. So, your application’s memory is limited by the proc- ess’s virtual address space. In a 32-bit process, you can allocate close to 1.5 gigabytes (GB) and in a 64-bit process, you can allocate close to 8 terabytes.

C#’s new operator causes the CLR to perform the following steps:

 

1.Calculate the number of bytes required for the type’s fields (and all the fields it inherits from

its base types).

 

2.Add the bytes required for an object’s overhead. Each object has two overhead fields: a type object pointer and a sync block index. For a 32-bit application, each of these fields requires


32 bits, adding 8 bytes to each object. For a 64-bit application, each field is 64 bits, adding 16

bytes to each object.

 

3.The CLR then checks that the bytes required to allocate the object are available in the region. If there is enough free space in the managed heap, the object will fit, starting at the address pointed to by NextObjPtr, and these bytes are zeroed out. The type’s constructor is called (passing NextObjPtr for the this parameter), and the new operator returns a reference to the object. Just before the reference is returned, NextObjPtr is advanced past the object and now points to the address where the next object will be placed in the heap.

Figure 21-1 shows a managed heap consisting of three objects: A, B, and C. If another object were to be allocated, it would be placed where NextObjPtr points to (immediately after object C).

 

A B C  

 

NextObjPtr

FIGURE 21-1Newly initialized managed heap with three objects constructed in it.

 

For the managed heap, allocating an object simply means adding a value to a pointer—this is blazingly fast. In many applications, objects allocated around the same time tend to have strong rela- tionships to each other and are frequently accessed around the same time. For example, it’s very com- mon to allocate a FileStream object immediately before a BinaryWriter object is created. Then the application would use the BinaryWriter object, which internally uses the FileStream object.

Because the managed heap allocates these objects next to each other in memory, you get excellent performance when accessing these objects due to locality of reference. Specifically, this means that your process’s working set is small, which means your application runs fast with less memory. It’s also likely that the objects your code is accessing can all reside in the CPU’s cache. The result is that your application will access these objects with phenomenal speed because the CPU will be able to perform most of its manipulations without having cache misses that would force slower access to RAM.

So far, it sounds like the managed heap provides excellent performance characteristics. However, what I have just described is assuming that memory is infinite and that the CLR can always allocate new objects at the end. However, memory is not infinite and so the CLR employs a technique known as garbage collection (GC) to “delete” objects in the heap that your application no longer requires access to.

 

The Garbage Collection Algorithm

When an application calls the new operator to create an object, there might not be enough address

space left in the region to allocate the object. If insufficient space exists, then the CLR performs a GC.


 

For managing the lifetime of objects, some systems use a reference counting algorithm. In fact, Microsoft’s own Component Object Model (COM) uses reference counting. With a reference count- ing system, each object on the heap maintains an internal field indicating how many “parts” of the program are currently using that object. As each “part” gets to a place in the code where it no longer requires access to an object, it decrements that object’s count field. When the count field reaches 0, the object deletes itself from memory. The big problem with many reference counting systems is that they do not handle circular references well. For example, in a GUI application, a window will hold a reference to a child UI element. And the child UI element will hold a reference to its parent window. These references prevent the two objects’ counters from reaching 0, so both objects will never be deleted even if the application itself no longer has a need for the window.

Due to this problem with reference counting garbage collector algorithms, the CLR uses a ref- erencing tracking algorithm instead. The reference tracking algorithm cares only about reference type variables, because only these variables can refer to an object on the heap; value type variables contain the value type instance directly. Reference type variables can be used in many contexts: static and instance fields within a class or a method’s arguments or local variables. We refer to all reference type variables as roots.

When the CLR starts a GC, the CLR first suspends all threads in the process. This prevents threads from accessing objects and changing their state while the CLR examines them. Then, the CLR per- forms what is called the marking phase of the GC. First, it walks through all the objects in the heap setting a bit (contained in the sync block index field) to 0. This indicates that all objects should be deleted. Then, the CLR looks at all active roots to see which objects they refer to. This is what makes the CLR’s GC a reference tracking GC. If a root contains null, the CLR ignores the root and moves on to examine the next root.

Any root referring to an object on the heap causes the CLR to mark that object. Marking an object means that the CLR sets the bit in the object’s sync block index to 1. When an object is marked, the CLR examines the roots inside that object and marks the objects they refer to. If the CLR is about to mark an already-marked object, then it does not examine the object’s fields again. This prevents an infinite loop from occurring in the case where you have a circular reference.

Figure 21-2 shows a heap containing several objects. In this example, the application roots refer directly to objects A, C, D, and F. All of these objects are marked. When marking object D, the gar- bage collector notices that this object contains a field that refers to object H, causing object H to be marked as well. The marking phase continues until all the application roots have been examined.


Once complete, the heap contains some marked and some unmarked objects. The marked objects must survive the collection because there is at least one root that refers to the object; we say that the object is reachable because application code can reach (or access) the object by way of the variable that still refers to it. Unmarked objects are unreachable because there is no root existing in the ap- plication that would allow for the object to ever be accessed again.

 
 

NextObjPtr

FIGURE 21-2Managed heap before a collection.

 

Now that the CLR knows which objects must survive and which objects can be deleted, it begins the GC’s compacting phase. During the compacting phase, the CLR shifts the memory consumed by the marked objects down in the heap, compacting all the surviving objects together so that they are contiguous in memory. This serves many benefits. First, all the surviving objects will be next to

each other in memory; this restores locality of reference reducing your application’s working set size, thereby improving the performance of accessing these objects in the future. Second, the free space is all contiguous as well, so this region of address space can be freed, allowing other things to use it. Finally, compaction means that there are no address space fragmentation issues with the managed heap as is known to happen with native heaps.1

When compacting memory, the CLR is moving objects around in memory. This is a problem be- cause any root that referred to a surviving object now refers to where that object was in memory; not where the object has been relocated to. When the application’s threads eventually get resumed, they would access the old memory locations and corrupt memory. Clearly, this can’t be allowed and so, as part of the compacting phase, the CLR subtracts from each root the number of bytes that the object it referred to was shifted down in memory. This ensures that every root refers to the same object it did before; it’s just that the object is at a different location in memory.

After the heap memory is compacted, the managed heap’s NextObjPtr pointer is set to point to a location just after the last surviving object. This is where the next allocated object will be placed

in memory. Figure 21-3 shows the managed heap after the compaction phase. After the compac- tion phase is complete, the CLR resumes all the application’s threads and they continue to access the objects as if the GC never happened at all.

 

 
 

1 Objects in the large object heap (discussed later in this chapter) do not get compacted, and therefore address space fragmentation is possible with the large object heap.


NextObjPtr

FIGURE 21-3Managed heap after a collection.

 

If the CLR is unable to reclaim any memory after a GC and if there is no address space left in the processes to allocate a new GC segment, then there is just no more memory available for this process. In this case, the new operator that attempted to allocate more memory ends up throwing an Out­ OfMemoryException. Your application can catch this and recover from it but most applications do not attempt to do so; instead, the exception becomes an unhandled exception, Windows terminates the process, and then Windows reclaims all the memory that the process was using.

As a programmer, notice how the two bugs described at the beginning of this chapter no longer exist. First, it’s not possible to leak objects because any object not accessible from your application’s roots will be collected at some point. Second, it’s not possible to corrupt memory by accessing an object that was freed because references can only refer to living objects, because this is what keeps the objects alive anyway.

       
   
 
 

 

 

Garbage Collections and Debugging

As soon as a root goes out of scope, the object it refers to is unreachable and subject to having its memory reclaimed by a GC; objects aren’t guaranteed to live throughout a method’s lifetime. This can have an interesting impact on your application. For example, examine the following code.

 

using System;

using System.Threading;

 

public static class Program { public static void Main() {

// Create a Timer object that knows to call our TimerCallback

// method once every 2000 milliseconds.

Timer t = new Timer(TimerCallback, null, 0, 2000);


// Wait for the user to hit <Enter>. Console.ReadLine();

}

 

private static void TimerCallback(Object o) {

// Display the date/time when this method got called. Console.WriteLine("In TimerCallback: " + DateTime.Now);

 

// Force a garbage collection to occur for this demo. GC.Collect();

}

}

 

Compile this code from the command prompt without using any special compiler switches. When

you run the resulting executable file, you’ll see that the TimerCallback method is called just once!

 

From examining the preceding code, you’d think that the TimerCallback method would get called once every 2,000 milliseconds. After all, a Timer object is created, and the variable t refers to this object. As long as the timer object exists, the timer should keep firing. But you’ll notice in the TimerCallback method that I force a garbage collection to occur by calling GC.Collect().

When the collection starts, it first assumes that all objects in the heap are unreachable (garbage); this includes the Timer object. Then, the collector examines the application’s roots and sees that Main doesn’t use the t variable after the initial assignment to it. Therefore, the application has no variable referring to the Timer object, and the garbage collection reclaims the memory for it; this stops the timer and explains why the TimerCallback method is called just once.

Let’s say that you’re using a debugger to step through Main, and a garbage collection just hap- pens to occur just after t is assigned the address of the new Timer object. Then, let’s say that you try to view the object that t refers to by using the debugger’s Quick Watch window. What do you think will happen? The debugger can’t show you the object because it was just garbage collected. This behavior would be considered very unexpected and undesirable by most developers, so Microsoft has come up with a solution.

When you compile your assembly by using the C# compiler’s /debug switch, the compiler applies a System.Diagnostics.DebuggableAttribute with its DebuggingModes’ DisableOptimizations flag set into the resulting assembly. At run time, when compiling a method, the JIT compiler sees this flag set, and artificially extends the lifetime of all roots to the end of the method. For my example, the JIT compiler tricks itself into believing that the t variable in Main must live until the end of the method. So, if a garbage collection were to occur, the garbage collector now thinks that t is still a root and that the Timer object that t refers to will continue to be reachable. The Timer object will survive the collection, and the TimerCallback method will get called repeatedly until Console.

ReadLine returns and Main exits.

 

To see this, just recompile the program from a command prompt, but this time, specify the C# compiler’s /debug switch. When you run the resulting executable file, you’ll now see that the Timer­ Callback method is called repeatedly! Note, the C# compiler’s /optimize+ compiler switch turns op- timizations back on, so this compiler switch should not be specified when performing this experiment.


The JIT compiler does this to help you with JIT debugging. You may now start your application normally (without a debugger), and if the method is called, the JIT compiler will artificially extend the lifetime of the variables to the end of the method. Later, if you decide to attach a debugger to the process, you can put a breakpoint in a previously compiled method and examine the root variables.

So now you know how to build a program that works in a debug build but doesn’t work correctly when you make a release build! Because no developer wants a program that works only when debug- ging it, there should be something we can do to the program so that it works all of the time regard- less of the type of build.

You could try modifying the Main method to the following.

 

public static void Main() {

// Create a Timer object that knows to call our TimerCallback

// method once every 2000 milliseconds.

Timer t = new Timer(TimerCallback, null, 0, 2000);

 

// Wait for the user to hit <Enter>. Console.ReadLine();

 

// Refer to t after ReadLine (this gets optimized away) t = null;

}

 

However, if you compile this (without the /debug+ switch) and run the resulting executable file, you’ll see that the TimerCallback method is still called just once. The problem here is that the JIT compiler is an optimizing compiler, and setting a local variable or parameter variable to null is the same as not referencing the variable at all. In other words, the JIT compiler optimizes the t = null; line out of the code completely, and therefore, the program still does not work as we desire. The cor- rect way to modify the Main method is as follows.

 

public static void Main() {

// Create a Timer object that knows to call our TimerCallback

// method once every 2000 milliseconds.

Timer t = new Timer(TimerCallback, null, 0, 2000);

 

// Wait for the user to hit <Enter>. Console.ReadLine();

 

// Refer to t after ReadLine (t will survive GCs until Dispose returns) t.Dispose();

}

 

Now, if you compile this code (without the /debug+ switch) and run the resulting executable file, you’ll see that the TimerCallback method is called multiple times, and the program is fixed. What’s happening here is that the object t is required to stay alive so that the Dispose instance method can be called on it. (The value in t needs to be passed as the this argument to Dispose.) It’s ironic: by explicitly indicating where you want the timer to be disposed, it must remain alive up to that point.


 

 
 

Generations: Improving Performance

The CLR’s GC is a generational garbage collector (also known as an ephemeral garbage collector, al- though I don’t use the latter term in this book). A generational GC makes the following assumptions about your code:

■ The newer an object is, the shorter its lifetime will be.

 

■ The older an object is, the longer its lifetime will be.

 

■ Collecting a portion of the heap is faster than collecting the whole heap.

 

Numerous studies have demonstrated the validity of these assumptions for a very large set of ex- isting applications, and these assumptions have influenced how the garbage collector is implemented. In this section, I’ll describe how generations work.

When initialized, the managed heap contains no objects. Objects added to the heap are said to be in generation 0. Stated simply, objects in generation 0 are newly constructed objects that the garbage collector has never examined. Figure 21-4 shows a newly started application with five objects allo- cated (A through E). After a while, objects C and E become unreachable.

 

A B C D E  

 

Generation 0

FIGURE 21-4A newly initialized heap containing some objects, all in generation 0. No collections have occurred yet.

 

When the CLR initializes, it selects a budget size (in kilobytes) for generation 0. So if allocating a new object causes generation 0 to surpass its budget, a garbage collection must start. Let’s say that objects A through E fill all of generation 0. When object F is allocated, a garbage collection must start.


The garbage collector will determine that objects C and E are garbage and will compact object D, caus- ing it to be adjacent to object B. The objects that survive the garbage collection (objects A, B, and D) are said to be in generation 1. Objects in generation 1 have been examined by the garbage collector once. The heap now looks like Figure 21-5.

 

A B D  

 


Gener-

ation 1


Generation 0


FIGURE 21-5After one collection, generation 0 survivors are promoted to generation 1; generation 0 is empty.

 

After a garbage collection, generation 0 contains no objects. As always, new objects will be al- located in generation 0. Figure 21-6 shows the application running and allocating objects F through

K. In addition, while the application was running, objects B, H, and J became unreachable and should have their memory reclaimed at some point.

 

A B D F G H I J K  

 


Gener-

ation 1


Generation 0


FIGURE 21-6New objects are allocated in generation 0; generation 1 has some garbage.

 

Now let’s say that attempting to allocate object L would put generation 0 over its budget. Because generation 0 has reached its budget, a garbage collection must start. When starting a garbage col- lection, the garbage collector must decide which generations to examine. Earlier, I said that when the CLR initializes, it selects a budget for generation 0. Well, it also selects a budget for generation 1.

When starting a garbage collection, the garbage collector also sees how much memory is occu- pied by generation 1. In this case, generation 1 occupies much less than its budget, so the garbage collector examines only the objects in generation 0. Look again at the assumptions that the genera- tional garbage collector makes. The first assumption is that newly created objects have a short life- time. So generation 0 is likely to have a lot of garbage in it, and collecting generation 0 will therefore reclaim a lot of memory. The garbage collector will just ignore the objects in generation 1, which will speed up the garbage collection process.

Obviously, ignoring the objects in generation 1 improves the performance of the garbage collec- tor. However, the garbage collector improves performance more because it doesn’t traverse every object in the managed heap. If a root or an object refers to an object in an old generation, the gar- bage collector can ignore any of the older objects’ inner references, decreasing the amount of time required to build the graph of reachable objects. Of course, it’s possible that an old object’s field re- fers to a new object. To ensure that the updated fields of these old objects are examined, the garbage collector uses a mechanism internal to the JIT compiler that sets a bit when an object’s reference field changes. This support lets the garbage collector know which old objects (if any) have been written to


because the last collection. Only old objects that have had fields change need to be examined to see

whether they refer to any new object in generation 0.2

       
   
 
 

 

A generational garbage collector also assumes that objects that have lived a long time will con- tinue to live. So it’s likely that the objects in generation 1 will continue to be reachable from the ap- plication. Therefore, if the garbage collector were to examine the objects in generation 1, it probably wouldn’t find a lot of garbage. As a result, it wouldn’t be able to reclaim much memory. So it is likely that collecting generation 1 is a waste of time. If any garbage happens to be in generation 1, it just stays there. The heap now looks like Figure 21-7.

 

A B D F G I K  

 

Generation 1 Generation 0

FIGURE 21-7After two collections, generation 0 survivors are promoted to generation 1 (growing the size of generation 1); generation 0 is empty.

 

As you can see, all of the generation 0 objects that survived the collection are now part of genera- tion 1. Because the garbage collector didn’t examine generation 1, object B didn’t have its memory reclaimed even though it was unreachable at the time of the last garbage collection. Again, after a collection, generation 0 contains no objects and is where new objects will be placed. In fact, let’s say that the application continues running and allocates objects L through O. And while running, the application stops using objects G, L, and M, making them all unreachable. The heap now looks like Figure 21-8.

 

A B D F G I K L M N O  

 

Generation 1 Generation 0

FIGURE 21-8New objects are allocated in generation 0; generation 1 has more garbage.

 

Let’s say that allocating object P causes generation 0 to exceed its budget, causing a garbage collection to occur. Because the memory occupied by all of the objects in generation 1 is less than its

 
 

2 For the curious, here are some more details about this. When the JIT compiler produces native code that modifies a ref- erence field inside an object, the native code includes a call to a write barrier method. This write barrier method checks whether the object whose field is being modified is in generation 1 or 2 and if it is, the write barrier code sets a bit in what is called the card table. The card table has 1 bit for every 128-byte range of data in the heap. When the next GC starts, it scans the card table to know which objects in generations 1 and 2 have had their fields changed because the last GC. If any of these modified objects refer to an object in generation 0, then the generation 0 objects survive the col- lection. After the GC, the card table is reset to all zeroes. The write barrier code causes a slight performance hit when writing to a reference field in an object (as opposed to a local variable or static field) and that performance hit is slightly worse if that object is in generation 1 or 2.


budget, the garbage collector again decides to collect only generation 0, ignoring the unreachable objects in generation 1 (objects B and G). After the collection, the heap looks like Figure 21-9.

 

A B D F G I K N O  

 

Generation 1 Generation 0

FIGURE 21-9After three collections, generation 0 survivors are promoted to generation 1 (growing the size of generation 1 again); generation 0 is empty.

 

In Figure 21-9, you see that generation 1 keeps growing slowly. In fact, let’s say that generation 1 has now grown to the point in which all of the objects in it occupy its full budget. At this point, the application continues running (because a garbage collection just finished) and starts allocating ob- jects P through S, which fill generation 0 up to its budget. The heap now looks like Figure 21-10.

 

A B D F G I K N O P Q R S  

 

Generation 1 Generation 0

FIGURE 21-10New objects are allocated in generation 0; generation 1 has more garbage.

 

When the application attempts to allocate object T, generation 0 is full, and a garbage collection must start. This time, however, the garbage collector sees that the objects in generation 1 are occupying so much memory that generation 1’s budget has been reached. Over the several generation 0 collec- tions, it’s likely that a number of objects in generation 1 have become unreachable (as in our example). So this time, the garbage collector decides to examine all of the objects in generation 1 and generation

0. After both generations have been garbage collected, the heap now looks like Figure 21-11.

 

D F I N O Q S  

 


Generation 2


Gener-

ation 1


Generation 0


FIGURE 21-11After four collections: generation 1 survivors are promoted to generation 2, generation 0 survivors are promoted to generation 1, and generation 0 is empty.

 

As before, any objects that were in generation 0 that survived the garbage collection are now in generation 1; any objects that were in generation 1 that survived the collection are now in generation

2. As always, generation 0 is empty immediately after a garbage collection and is where new objects will be allocated. Objects in generation 2 are objects that the garbage collector has examined two or more times. There might have been several collections, but the objects in generation 1 are examined only when generation 1 reaches its budget, which usually requires several garbage collections of generation 0.


The managed heap supports only three generations: generation 0, generation 1, and genera- tion 2; there is no generation 3.3 When the CLR initializes, it selects budgets for all three generations. However, the CLR’s garbage collector is a self-tuning collector. This means that the garbage collector learns about your application’s behavior whenever it performs a garbage collection. For example, if your application constructs a lot of objects and uses them for a very short period of time, it’s pos- sible that garbage collecting generation 0 will reclaim a lot of memory. In fact, it’s possible that the memory for all objects in generation 0 can be reclaimed.

If the garbage collector sees that there are very few surviving objects after collecting generation 0, it might decide to reduce the budget of generation 0. This reduction in the allotted space will mean that garbage collections occur more frequently but will require less work for the garbage collector, so your process’s working set will be small. In fact, if all objects in generation 0 are garbage, a garbage collection doesn’t have to compact any memory; it can simply set NextObjPtr back to the begin- ning of generation 0, and then the garbage collection is performed. Wow, this is a fast way to reclaim memory!

       
   
 
 

 

On the other hand, if the garbage collector collects generation 0 and sees that there are a lot of surviving objects, not a lot of memory was reclaimed in the garbage collection. In this case, the gar- bage collector will grow generation 0’s budget. Now, fewer collections will occur, but when they do, a lot more memory should be reclaimed. By the way, if insufficient memory has been reclaimed after a collection, the garbage collector will perform a full collection before throwing an OutOfMemory­ Exception.

Throughout this discussion, I’ve been talking about how the garbage collector dynamically modi- fies generation 0’s budget after every collection. But the garbage collector also modifies the budgets of generation 1 and generation 2 by using similar heuristics. When these generations are garbage collected, the garbage collector again sees how much memory is reclaimed and how many objects survived. Based on the garbage collector’s findings, it might grow or shrink the thresholds of these

 

 
 

3 The System.GC class’s static MaxGeneration method returns 2.


generations as well to improve the overall performance of the application. The end result is that the garbage collector fine-tunes itself automatically based on the memory load required by your applica- tion—this is very cool!

The following GCNotification class raises an event whenever a generation 0 or generation 2 collection occurs. With these events, you could have the computer beep whenever a collection occurs or you calculate how much time passes between collections, how much memory is allocated between collections, and more. With this class, you could easily instrument your application to get a better understanding of how your application uses memory.

 

public static class GCNotification {

private static Action<Int32> s_gcDone = null; // The event's field

 

public static event Action<Int32> GCDone { add {

// If there were no registered delegates before, start reporting notifications now if (s_gcDone == null) { new GenObject(0); new GenObject(2); }

s_gcDone += value;

}

remove { s_gcDone ­= value; }

}

 

private sealed class GenObject { private Int32 m_generation;

public GenObject(Int32 generation) { m_generation = generation; }

~GenObject() { // This is the Finalize method

// If this object is in the generation we want (or higher),

// notify the delegates that a GC just completed if (GC.GetGeneration(this) >= m_generation) {

Action<Int32> temp = Volatile.Read(ref s_gcDone); if (temp != null) temp(m_generation);

}

 

// Keep reporting notifications if there is at least one delegate registered,

// the AppDomain isn't unloading, and the process isn’t shutting down if ((s_gcDone != null)

&& !AppDomain.CurrentDomain.IsFinalizingForUnload() && !Environment.HasShutdownStarted) {

// For Gen 0, create a new object; for Gen 2, resurrect the object

// & let the GC call Finalize again the next time Gen 2 is GC'd if (m_generation == 0) new GenObject(0);

else GC.ReRegisterForFinalize(this);

} else { /* Let the objects go away */ }

}

}

}


Garbage Collection Triggers

As you know, the CLR triggers a GC when it detects that generation 0 has filled its budget. This is the

most common trigger of a GC; however, there are additional GC triggers as listed here:

 

Code explicitly calls System.GC’s static Collect methodCode can explicitly request that the CLR perform a collection. Although Microsoft strongly discourages such requests, at times it might make sense for an application to force a collection. I discuss this more in the “Forcing Garbage Collections” section later in this chapter.

Windows is reporting low memory conditionsThe CLR internally uses the Win32 Create­ MemoryResourceNotification and QueryMemoryResourceNotification functions to monitor system memory overall. If Windows reports low memory, the CLR will force a garbage collection in an effort to free up dead objects to reduce the size of a process’s working set.

The CLR is unloading an AppDomainWhen an AppDomain unloads, the CLR considers nothing in the AppDomain to be a root, and a garbage collection consisting of all generations is performed. I’ll discuss AppDomains in Chapter 22, “CLR Hosting and AppDomains.”

The CLR is shutting downThe CLR shuts down when a process terminates normally (as op- posed to an external shutdown via Task Manager, for example). During this shutdown, the CLR considers nothing in the process to be a root; it allows objects a chance to clean up but the CLR does not attempt to compact or free memory because the whole process is terminating, and Windows will reclaim all of the processes’ memory.

 

 

Large Objects

There is one more performance improvement you might want to be aware of. The CLR considers each single object to be either a small object or a large object. So far, in this chapter, I’ve been focusing

on small objects. Today, a large object is 85,000 bytes or more in size.4 The CLR treats large objects slightly differently than how it treats small objects:

■ Large objects are not allocated within the same address space as small objects; they are al- located elsewhere within the process’ address space.

■ Today, the GC doesn’t compact large objects because of the time it would require to move them in memory. For this reason, address space fragmentation can occur between large objects within the process leading to an OutOfMemoryException being thrown. In a future version of the CLR, large objects may participate in compaction.

 

 

 
 

4 In the future, the CLR could change the number of bytes required to consider an object to be a large object. Do not count 85,000 being a constant.


■ Large objects are immediately considered to be part of generation 2; they are never in gen- eration 0 or 1. So, you should create large objects only for resources that you need to keep alive for a long time. Allocating short-lived large objects will cause generation 2 to be col- lected more frequently, hurting performance. Usually large objects are large strings (like XML or JSON) or byte arrays that you use for I/O operations, such as reading bytes from a file or network into a buffer so you can process it.

For the most part, large objects are transparent to you; you can simply ignore that they exist and that they get special treatment until you run into some unexplained situation in your program (like why you’re getting address space fragmentation).

 

Garbage Collection Modes

When the CLR starts, it selects a GC mode, and this mode cannot change during the lifetime of the process. There are two basic GC modes:

WorkstationThis mode fine-tunes the garbage collector for client-side applications. It is op- timized to provide for low-latency GCs in order to minimize the time an application’s threads are suspended so as not to frustrate the end user. In this mode, the GC assumes that other applications are running on the machine and does not hog CPU resources.

ServerThis mode fine-tunes the garbage collector for server-side applications. It is opti- mized for throughput and resource utilization. In this mode, the GC assumes no other appli- cations (client or server) are running on the machine, and it assumes that all the CPUs on the machine are available to assist with completing the GC. This GC mode causes the managed heap to be split into several sections, one per CPU. When a garbage collection is initiated, the garbage collector dedicates one special thread per CPU; each thread collects its own section in parallel with the other threads. Parallel collections work well for server applications in which the worker threads tend to exhibit uniform behavior. This feature requires the application to be running on a computer with multiple CPUs so that the threads can truly be working simul- taneously to attain a performance improvement.

 

By default, applications run with the Workstation GC mode. A server application (such as ASP.NET or Microsoft SQL Server) that hosts the CLR can request the CLR to load the Server GC. However, if the server application is running on a uniprocessor machine, then the CLR will always use Workstation GC mode. A stand-alone application can tell the CLR to use the Server GC mode by creating a configura- tion file (as discussed in Chapter 2, “Building, Packaging, Deploying, and Administering Applications and Types,” and Chapter 3, “Shared Assemblies and Strongly Named Assemblies”) that contains a gcServer element for the application. Here’s an example of a configuration file.

 

<configuration>

<runtime>

<gcServer enabled="true"/>

</runtime>

</configuration>


When an application is running, it can ask the CLR if it is running in the Server GC mode by query- ing the GCSettings class’s IsServerGC read-only Boolean property.

 

using System;

using System.Runtime; // GCSettings is in this namespace

 

public static class Program { public static void Main() {

Console.WriteLine("Application is running with server GC=" + GCSettings.IsServerGC);

}

}

 

In addition to the two modes, the GC can run in two sub-modes: concurrent (the default) or non- concurrent. In concurrent mode, the GC has an additional background thread that marks objects concurrently while the application runs. When a thread allocates an object that pushes generation 0 over its budget, the GC first suspends all threads and then determines which generations to collect. If the garbage collector needs to collect generation 0 or 1, it proceeds as normal. However, if genera- tion 2 needs collecting, the size of generation 0 will be increased beyond its budget to allocate the new object, and then the application’s threads are resumed.

While the application’s threads are running, the garbage collector has a normal priority back- ground thread that finds unreachable objects. Once found, the garbage collector suspends all threads again and decides whether to compact memory. If the garbage collector decides to com- pact memory, memory is compacted, root references are fixed up, and the application’s threads are resumed. This garbage collection takes less time than usual because the set of unreachable objects has already been built. However, the garbage collector might decide not to compact memory; in fact, the garbage collector favors this approach. If you have a lot of free memory, the garbage collector won’t compact the heap; this improves performance but grows your application’s working set. When using the concurrent garbage collector, you’ll typically find that your application is consuming more memory than it would with the non-concurrent garbage collector.

You can tell the CLR not to use the concurrent collector by creating a configuration file for the ap- plication that contains a gcConcurrent element. Here’s an example of a configuration file.

 

<configuration>

<runtime>

<gcConcurrent enabled="false"/>

</runtime>

</configuration>

 

The GC mode is configured for a process and it cannot change while the process runs. However, your application can have some control over the garbage collection by using the GCSettings class’s GCLatencyMode property. This read/write property can be set to any of the values in the GCLatency­ Mode enumerated type, as shown in Table 21-1.

The LowLatency mode requires some additional explanation. Typically, you would set this mode, perform a short-term, time-sensitive operation, and then set the mode back to either Batch or In­ teractive. While the mode is set to LowLatency, the GC will really avoid doing any generation 2


collections because these could take a long time. Of course, if you call GC.Collect(), then genera- tion 2 still gets collected. Also, the GC will perform a generation 2 collection if Windows tells the CLR that system memory is low (see the “Garbage Collection Triggers” section earlier in this chapter).

 

TABLE 21-1Symbols Defined by the GCLatencyMode Enumerated Type

 

Symbol Name Description
Batch (default for the Server GC mode) Turns off the concurrent GC.
Interactive (default for the Workstation GC mode) Turns on the concurrent GC.
LowLatency Use this latency mode during short-term, time-sensitive operations (like drawing ani- mations) where a generation 2 collection might be disruptive.
SustainedLowLatency Use this latency mode to avoid long GC pauses for the bulk of your application’s execu- tion. This setting prevents all blocking generation 2 collections from occurring as long as memory is available. In fact, users of these applications would prefer to install more RAM in the machine in order to avoid GC pauses. A stock market application that must respond immediately to price changes is an example of this kind of application.

 

Under LowLatency mode, it is more likely that your application could get an OutOfMemory­ Exception thrown. Therefore, stay in this mode for as short a time as possible, avoid allocating many objects, avoid allocating large objects, and set the mode back to Batch or Interactive by using a constrained execution region (CER), as discussed in Chapter 20, “Exceptions and State Management.” Also, remember that the latency mode is a process-wide setting and threads may be running concur- rently. These other threads could even change this setting while another thread is using it, so you may want to update some kind of counter (manipulated via Interlocked methods) when you have mul- tiple threads manipulating this setting. Here is some code showing how to use the LowLatency mode.

 

private static void LowLatencyDemo() { GCLatencyMode oldMode = GCSettings.LatencyMode;

System.Runtime.CompilerServices.RuntimeHelpers.PrepareConstrainedRegions(); try {

GCSettings.LatencyMode = GCLatencyMode.LowLatency;

// Run your code here...

}

finally {

GCSettings.LatencyMode = oldMode;

}

}

 

 

Forcing Garbage Collections

The System.GC type allows your application some direct control over the garbage collector. For starters, you can query the maximum generation supported by the managed heap by reading the GC.MaxGeneration property; this property always returns 2.

You can also force the garbage collector to perform a collection by calling GC class’s Collect

method, optionally passing in a generation to collect up to, a GCCollectionMode, and a Boolean


indicating whether you want to perform a blocking (non-current) or background (concurrent) collec- tion. Here is the signature of the most complex overload of the Collect method.

 

void Collect(Int32 generation, GCCollectionMode mode, Boolean blocking);

 

The GCCollectionMode type is an enum whose values are described in Table 21-2.

 

TABLE 21-2Symbols Defined by the GCCollectionMode Enumerated Type

 

Symbol Name Description
Default The same as calling GC.Collect with no flag. Today, this is the same as passing Forced, but this may change in a future version of the CLR.
Forced Forces a collection to occur immediately for all generations up to and including the specified generation.
Optimized The garbage collector will only perform a collection if the collection would be productive either by freeing a lot of memory or by reducing fragmentation. If the garbage collection would not be productive, then the call has no effect

 

Under most circumstances, you should avoid calling any of the Collect methods; it’s best just to let the garbage collector run on its own accord and fine-tune its generation budgets based on actual application behavior. However, if you’re writing a console user interface (CUI) or GUI applica- tion, your application code owns the process and the CLR in that process. For these application types, you might want to suggest a garbage collection to occur at certain times using a GCCollectionMode of Optimized. Normally, modes of Default and Forced are used for debugging, testing, and look- ing for memory leaks.

For example, you might consider calling the Collect method if some non-recurring event has just occurred that has likely caused a lot of old objects to die. The reason that calling Collect in such a circumstance may not be so bad is that the GC’s predictions of the future based on the past are not likely to be accurate for non-recurring events. For example, it might make sense for your applica-

tion to force a full GC of all generations after your application initializes or after the user saves a data file. Because calling Collect causes the generation budgets to adjust, do not call Collect to try to improve your application’s response time; call it to reduce your process’s working set.

For some applications (especially server applications that tend to keep a lot of objects in memory), the time required for the GC to do a full collection that includes generation 2 can be excessive. In fact, if the collection takes a very long time to complete, then client requests might time out. To help these kinds of applications, the GC class offers a RegisterForFullGCNotification method. Using this method and some additional helper methods (WaitForFullGCApproach, WaitForFullGC­

Complete, and CancelFullGCNotification), an application can now be notified when the garbage

collector is getting close to performing a full collection. The application can then call GC.Collect to force a collection at a more opportune time, or the application could communicate with another server to better load balance the client requests. For more information, examine these methods and the “Garbage Collection Notifications” topic in the Microsoft .NET Framework SDK documentation. Note that you should always call the WaitForFullGCApproach and WaitForFullGCComplete methods in pairs because the CLR handles them as pairs internally.


Monitoring Your Application’s Memory Usage

Within a process, there are a few methods that you can call to monitor the garbage collector. Specifi- cally, the GC class offers the following static methods, which you can call to see how many collections have occurred of a specific generation or how much memory is currently being used by objects in the managed heap.

 

Int32 CollectionCount(Int32 generation);

Int64 GetTotalMemory(Boolean forceFullCollection);

 

To profile a particular code block, I have frequently written code to call these methods before and after the code block and then calculate the difference. This gives me a very good indication of how my code block has affected my process’s working set and indicates how many garbage collections occurred while executing the code block. If the numbers are high, I know to spend more time tuning the algorithms in my code block.

You can also see how much memory is being used by individual AppDomains as opposed to the whole process. For more information about this, see the “AppDomain Monitoring” section in Chapter 22.

When you install the .NET Framework, it installs a set of performance counters that offer a lot of real-time statistics about the CLR’s operations. These statistics are visible via the PerfMon.exe tool or the System Monitor ActiveX control that ships with Windows. The easiest way to access the System Monitor control is to run PerfMon.exe and click the + toolbar button, which causes the Add Counters dialog box shown in Figure 21-12 to appear.

 
 

FIGURE 21-12PerfMon.exe showing the .NET CLR Memory counters.

 

To monitor the CLR’s garbage collector, select the .NET CLR Memory performance object. Then select a specific application from the instance list box. Finally, select the set of counters that you’re interested in monitoring, click Add, and then click OK. At this point, the System Monitor will graph the


selected real-time statistics. For an explanation of a particular counter, select the desired counter and then select the Show Description check box.

Another great tool for analyzing the memory and performance of your application is PerfView. This tool can collect Event Tracing for Windows (ETW) logs and process them. The best way to acquire this tool is for you to search the web for PerfView. Finally, you should look into using the SOS Debugging Extension (SOS.dll), which can often offer great assistance when debugging memory problems and other CLR problems. For memory-related actions, the SOS Debugging Extension allows you to see how much memory is allocated within the process to the managed heap, displays all objects registered for finalization in the finalization queue, displays the entries in the GCHandle table per AppDomain or for the entire process, shows the roots that are keeping an object alive in the heap, and more.

 

 


Date: 2016-03-03; view: 612


<== previous page | next page ==>
Nbsp;   Code Contracts | Nbsp;   Working with Types Requiring Special Cleanup
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.058 sec.)