Memory Management


The CLR provides automatic memory management. Essentially, this means that when developers create a value (i.e., an instance of any type), the CLR is responsible for allocating and freeing the memory in which that value is stored. Value types and reference types have their memory allocated and freed via different mechanisms, however.

Value Types Versus Reference Types

Value types are normally allocated on the program's stack. Whenever a parameter or a local variable is needed, such as when a method is called, memory for the value is allocated on the stack. This memory remains available until the method returns; the stack is then unwound and the memory is automatically reclaimed. This model of allocating and freeing stack-based memory for parameters and local variables is very common in programming languages, being used by C, C++, and Java, for example. The mechanism is also simple to implement, because growing and freeing stack memory is as simple as adjusting a stack pointer during program execution. The major limitation with this approach is the fact that the lifetimes of values remain tied to the method invocation that creates them; when the method returns, the memory allocated for the values is reclaimed.

Reference types often have lifetimes that are not related to the methods that create them, so their allocation and freeing are a bit more complicated. Objects, which are values of Object types , are allocated on a heap. The lifetime of an object may differ from that specified by the method that allocates it. From this viewpoint, object allocation in the CLR resembles heap allocation in C or C++. In those programming languages, however, the allocated memory is reclaimed when free or delete is manually called on the memory. This mechanism requires the programmer to explicitly reclaim memory, which leads to two major issues:

  • Memory can be reclaimed while references still refer to an object. As long as no attempt is made to access the object, this situation does not normally cause any problems. By contrast, using a reference to access an object after its memory has been reclaimed may lead to undefined behavior from the program.

  • Programs may forget to free memory allocated for objects even when they are no longer being used. In this situation, programs may exhaust their available memory and fail.

Whenever an object is allocated in the CLR, the runtime environment takes charge of tracking all references to that object. It guarantees that while an object has a reference to it (i.e., while it is still reachable ), its memory will not be reclaimed. It also guarantees that once an object becomes unreachable, its memory will be reclaimed if available memory becomes scarce . This scenario provides for a good compromise between safety, performance, and memory usage. Programmers need not manage memory manually, which makes writing programs simpler, and memory is reclaimed as needed, thereby avoiding the overhead of reclaiming it unnecessarily. The problem with this scenario is that programs cannot be sure when, or even if, objects will have their memory reclaimed and, therefore, their destructors executed.

Garbage Collection

The algorithm for the garbage collector is extremely specialized, but the simplistic model described here should provide developers with a basic understanding of its operation.

The managed heap is a large area of memory in which the CLR sequentially allocates objects. As each object is allocated, it is placed in the heap just after the last object allocated on the heap. Because each object can be of a different size, the CLR adds the size of the new object to its starting location; this position indicates where the next object will be allocated. The CLR returns a reference to the object, and the program continues executing. When the next object is allocated, the CLR repeats the allocation algorithm.

At some point during program execution, the CLR may start reclaiming the memory allocated for unreachable objects. This situation may be triggered, for example, by the program calling GC.Collect() or by the runtime noticing that most of the available memory in the heap has been allocated. When such an event occurs, the garbage collector starts by assuming that all objects on the heap are unreachable; an object is considered "unreachable" when no references point to it. The garbage collector knows where primary object references can be stored ”for example, as local variables and parameters ”so it first locates all of these primary references and marks all objects referenced by them as "reachable." As an object is found to be reachable, its members are inspected for references to other objects, with each object that the current object refers to also being marked as reachable. [4]

[4] This book calls the second type of references "secondary references," which is purely our own terminology. The reason for distinguishing between primary and secondary references is that two objects may share a cycle where they each refer to each other ”that is, they may have secondary references to each other. If neither of these objects has a primary reference, then clearly the objects can be removed without causing any problems.

After it has identified all reachable objects, the garbage collector examines all unreachable objects to see whether their finalization methods should be executed. If so, then a reference to the object is added to the list of objects requiring finalization. The object then becomes reachable again and, therefore, is not available for deletion until its finalization method executes. In general, although finalization methods sound appealing, they can create a number of problems. For example, these methods are never guaranteed to run, the order in which objects are finalized is not specified, and the use of finalization methods keeps objects in memory longer than necessary. Also, a finalization method may cause an object to be resurrected by creating a reference to the object that the garbage collector will see during its next operation. The interactions and possibilities seem endless. [5]

[5] A detailed discussion of the semantics of finalization is beyond the scope of this book. Refer to the SDK documentation for a more detailed specification.

At this point, the garbage collector deletes all unreachable objects by moving the reachable objects into a sequential array of memory, along the way overwriting any unreachable objects. This compaction minimizes memory usage as well as eliminates unused objects. To ensure that all object references remain valid, the garbage collector must update these references. After garbage collection, object references might point to another location in the garbage collected heap but they will still point to the same object.

The garbage collection algorithm described here is greatly simplified, but some optimizations can be applied to increase its efficiency. For instance, objects may be placed in generations, where a "generation" represents a garbage collection cycle. An object that has not been through a cycle is part of generation 0. After each cycle, each object's generation increases by one. Thus, objects in each generation are compacted together, and the generations are arranged from highest to lowest throughout the managed heap. Long-lived objects, such as those that persist for the entire life of the program, will be moved to one end of the managed heap and basically stay there. As a consequence, references to those objects will rarely need to be changed. As a performance optimization, garbage collection might work on only specific generations. The assumption here is that long-lived objects (i.e., those in higher generations) are more likely to persist and do not need to be subjected to testing for garbage collection as often as newly allocated objects.

Example: Generation-Based Garbage Collection

Listing 4.2 demonstrates how the generation-based garbage collection works. The program first displays the maximum number of generations for the system (three, ranging from generation 0 to generation 2, inclusive). Next, it allocates a new object and calls GC . Collect which takes a single parameter that specifies the number of generations to be processed . As this parameter is also the maximum number of generations supported by the system, objects in all generations are checked. This checking moves the only object allocated by the program ( p1 ) up by one generation. The allocation/aging cycle is repeated three more times. Eventually, the first object reaches generation 2 and cannot be aged anymore.

Listing 4.2 Generation-based garbage collection
 using System; using System.Runtime.InteropServices; public class GCSample {   public static int Main()   {     Console.WriteLine(         "Maximum Generations = {0}",          GC.MaxGeneration);     Object p1 = new Object();     Console.WriteLine(         "p1: {0}\n", GC.GetGeneration(p1));     GC.Collect();     Console.WriteLine(         "p1: {0}\n", GC.GetGeneration(p1));     Object p2 = new Object();     GC.Collect(0);     Console.WriteLine(         "p1: {0}", GC.GetGeneration(p1));     Console.WriteLine(         "p2: {0}\n", GC.GetGeneration(p2));     GC.Collect(1);     Console.WriteLine(         "p1: {0}", GC.GetGeneration(p1));     Console.WriteLine(         "p2: {0}\n", GC.GetGeneration(p2));     return 0;   } } 

Listing 4.2 produces the following output:

 Maximum Generations = 2  p1: 0 p1: 1 p1: 1 p2: 1 p1: 2 p2: 2 


Programming in the .NET Environment
Programming in the .NET Environment
ISBN: 0201770180
EAN: 2147483647
Year: 2002
Pages: 146

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net