Automatic Memory Management
Automatic Memory Management
Automatic memory management is one of the services that the common language runtime provides during
Managed Execution. The common language runtime's garbage collector manages the allocation and release
of memory for an application. For developers, this means that you do not have to write code to perform
memory management tasks when you develop managed applications. Automatic memory management can
eliminate common problems, such as forgetting to free an object and causing a memory leak, or
attempting to access memory for an object that has already been freed. This section describes how the
garbage collector allocates and releases memory.
Allocating Memory
When you initialize a new process, the runtime reserves a contiguous region of address space for the
process. This reserved address space is called the managed heap. The managed heap maintains a pointer to
the address where the next object in the heap will be allocated. Initially, this pointer is set to the managed
heap's base address. All reference types are allocated on the managed heap. When an application creates the
first reference type, memory is allocated for the type at the base address of the managed heap. When the
application creates the next object, the garbage collector allocates memory for it in the address space
immediately following the first object. As long as address space is available, the garbage collector continues
to allocate space for new objects in this manner.
Allocating memory from the managed heap is faster than unmanaged memory allocation. Because the
runtime allocates memory for an object by adding a value to a pointer, it is almost as fast as allocating
memory from the stack. In addition, because new objects that are allocated consecutively are stored
contiguously in the managed heap, an application can access the objects very quickly.
Releasing Memory
The garbage collector's optimizing engine determines the best time to perform a collection based on the
allocations being made. When the garbage collector performs a collection, it releases the memory for
objects that are no longer being used by the application. It determines which objects are no longer being
used by examining the application's roots. Every application has a set of roots. Each root either refers to an
object on the managed heap or is set to null. An application's roots include global and static object
pointers, local variables and reference object parameters on a thread's stack, and CPU registers. The
garbage collector has access to the list of active roots that the just-in-time (JIT) compiler and the runtime
maintain. Using this list, it examines an application's roots, and in the process creates a graph that contains
all the objects that are reachable from the roots.
Objects that are not in the graph are unreachable from the application's roots. The garbage collector
considers unreachable objects garbage and will release the memory allocated for them. During a collection,
the garbage collector examines the managed heap, looking for the blocks of address space occupied by
unreachable objects. As it discovers each unreachable object, it uses a memory-copying function to
compact the reachable objects in memory, freeing up the blocks of address spaces allocated to unreachable
objects. Once the memory for the reachable objects has been compacted, the garbage collector makes the
necessary pointer corrections so that the application's roots point to the objects in their new locations. It
also positions the managed heap's pointer after the last reachable object. Note that memory is compacted
only if a collection discovers a significant number of unreachable objects. If all the objects in the managed
heap survive a collection, then there is no need for memory compaction.
To improve performance, the runtime allocates memory for large objects in a separate heap. The garbage
collector automatically releases the memory for large objects. However, to avoid moving large objects in
memory, this memory is not compacted.
Generations and Performance
To optimize the performance of the garbage collector, the managed heap is divided into three generations:
0, 1, and 2. The runtime's garbage collection algorithm is based on several generalizations that the
computer software industry has discovered to be true by experimenting with garbage collection schemes.
First, it is faster to compact the memory for a portion of the managed heap than for the entire managed
heap. Secondly, newer objects will have shorter lifetimes and older objects will have longer lifetimes. Lastly,
newer objects tend to be related to each other and accessed by the application around the same time.
The runtime's garbage collector stores new objects in generation 0. Objects created early in the
application's lifetime that survive collections are promoted and stored in generations 1 and 2. The process
of object promotion is described later in this topic. Because it is faster to compact a portion of the
managed heap than the entire heap, this scheme allows the garbage collector to release the memory in a
specific generation rather than release the memory for the entire managed heap each time it performs a
collection.
In reality, the garbage collector performs a collection when generation 0 is full. If an application attempts to
create a new object when generation 0 is full, the garbage collector discovers that there is no address space
remaining in generation 0 to allocate for the object. The garbage collector performs a collection in an
attempt to free address space in generation 0 for the object. The garbage collector starts by examining the
objects in generation 0 rather than all objects in the managed heap. This is the most efficient approach,
because new objects tend to have short lifetimes, and it is expected that many of the objects in generation
0 will no longer be in use by the application when a collection is performed. In addition, a collection of
generation 0 alone often reclaims enough memory to allow the application to continue creating new
objects.
After the garbage collector performs a collection of generation 0, it compacts the memory for the reachable
objects as explained in Releasing Memory earlier in this topic. The garbage collector then promotes these
objects and considers this portion of the managed heap generation 1. Because objects that survive
collections tend to have longer lifetimes, it makes sense to promote them to a higher generation. As a
result, the garbage collector does not have to reexamine the objects in generations 1 and 2 each time it
performs a collection of generation 0.
After the garbage collector performs its first collection of generation 0 and promotes the reachable objects
to generation 1, it considers the remainder of the managed heap generation 0. It continues to allocate
memory for new objects in generation 0 until generation 0 is full and it is necessary to perform another
collection. At this point, the garbage collector's optimizing engine determines whether it is necessary to
examine the objects in older generations. For example, if a collection of generation 0 does not reclaim
enough memory for the application to successfully complete its attempt to create a new object, the
garbage collector can perform a collection of generation 1, then generation 2. If this does not reclaim
enough memory, the garbage collector can perform a collection of generations 2, 1, and 0. After each
collection, the garbage collector compacts the reachable objects in generation 0 and promotes them to
generation 1. Objects in generation 1 that survive collections are promoted to generation 2. Because the
garbage collector supports only three generations, objects in generation 2 that survive a collection remain
in generation 2 until they are determined to be unreachable in a future collection.
For the majority of the objects that your application creates, you can rely on the garbage collector to
automatically perform the necessary memory management tasks. However, unmanaged resources require
explicit cleanup. The most common type of unmanaged resource is an object that wraps an operating
system resource, such as a file handle, window handle, or network connection. Although the garbage
collector is able to track the lifetime of a managed object that encapsulates an unmanaged resource, it
does not have specific knowledge about how to clean up the resource. When you create an object that
encapsulates an unmanaged resource, it is recommended that you provide the necessary code to clean up
the unmanaged resource in a public Dispose method. By providing a Dispose method, you enable users of
your object to explicitly free its memory when they are finished with the object. When you use an object
that encapsulates an unmanaged resource, you should be aware of Dispose and call it as necessary. For
more information about cleaning up unmanaged resources and an example of a design pattern for
implementing Dispose, see Garbage Collection.
.Net automatically manages the memory using the Garbage Collector. Garbage Collector plays vital
role in the managed execution. While executing an application, variables and objects that we
declared in our application need some storage space in the memory. Class Loader loads the MSIL
into the runtime. IL to Native compiler compiles the IL into native code with help of JIT compiler,
which maintains the different compilers for the different OS. During the execution of the code,
code manager find the main method of the application, which is the entry point for an application.
At this stage objects are allocated into the memory. Now the Garbage Collector comes into the
picture. At the time or occurrence of the error called system memory out of range GC comes into
play.
During the execution, when the code manager found the new keyword, GC reserves some storage
space without allocation. This reserved space is called as managed heap. A pointer is maintained
here, that is used to points the address on which address space the object is going to be allocated.
At first, the pointer is pointed to the base address. When the object is allocated on a storage
space, the pointer moves to the top of that object where the next objects can be allocated.
Application maintains a set of application roots. Each root points to the storage space. This root
points either to the objects stored in memories or it is set to null. JIT compiler maintains the list of
roots. Garbage Collector uses this list to draw the graph on the objects. At first the GC considers
all managed memories as garbage. Then it navigate the memory and find whether memory is
referenced or not and then it marks the unreferenced memory as garbage. Now the GC collects
the released memory. Now the application root page has the invalid memory address. So it'll be
rearranged and maintains the right memory address detail.
GC uses a mechanism to release the memories occupied by the objects called as generations. GC
maintains the three generations such as generation 0, generation 1, & generation 2. The newly
created objects are stored in the generation 0. If the objects are survive during the garbage
collection it'll be pushed to the next higher generations. When there is no space available in the
generation 0, then GC will moved to the next generation to search for the garbage. This is the way
that the GC doing its job in order to have efficient memory management.