top of page
Writer's pictureThe Tech Platform

.NET Memory Management

When writing an application using C# and .NET framework, developers should be mindful of memory management to avoid memory leaks and write memory-aware code. This article will look at the basics anyone should know; mainly, we will try to answer questions on memory allocation and NET Garbage Collector. Finally, we will look at best practices.


Memory allocation

Memory can be allocated either in Stack or Heap. In .NET, we typically allocate all local variables in Stack, so when the first method calls the second method, then the return address of the first method (the one that calls) is stored in Stack. The control is passed to the second one. After the second method finishes execution, it is removed from Stack and all the data used by it; then execution returns to the first method. Unlike Stack, the Heap is used to store objects (references to those objects are stored in Stack) and global variables, static global variables and types (when we use new keyword).

At this point, we have to make an important note: in the case of multi-threading, each thread has its Stack. However, Heap is consistently shared among all the threads. That is why when writing a multi-threading application, developers must be aware of thread safeness to avoid race condition.

In the case of inheritance, when we create an object of a child class, a single object is created in Heap. This object would store all state-related data of classes, including the parent class.



Garbage Collector (GC)

Following Java language, the C# and .NET framework creators added the so-called Garbage Collector to clean up all the allocated objects automatically. A very convenient language feature for developers that checks allocated objects on Heap, which is not referenced by anything (in other words: have no so-called root reference). Heap keeps static variables which are never garbage collected because these never have root references. Keep in mind that Garbage Collector runs on a separate thread and collects the unused objects, and free up memory. It runs automatically and periodically, and when an application begins to run out of memory.

Garbage Collector Generations — based on an object’s life cycle, there are three generations (categories):

  • Generation 0 — newly created object is put in Generation 0 and has not been checked by Garbage Collector yet.

  • Generation 1 — object inspected by Garbage Collector once but kept in Generation 1 because having a root reference.

  • Generation 2 — if the object passes two or more inspections and is not terminated by Garbage Collector because of having a root reference are in Generation 2.

Garbage Collector does not collect objects of unmanaged resources like files or databases for that matter. For that, the developer have to call Dispose(); explicitly (when inheriting from IDisposable) or to use the concerned class object within using keyword (again, make sure IDiposable is inherited in the type you want to use dispose of for).

Important note: do not call Dispose(); method or use using a keyword on an object that is injected (it is already handled when using Dependency Injection).


An example of a full class that inherits from IDisposable follows (uses TestServer and HttpClient):

public class TestFixture<TStartup> : IDisposable
{
    public TestServer FServer { get; }
    public HttpClient FClient { get; }
    
    public void Dispose()    
    {
        FClient.Dispose();
        FServer.Dispose();    
    }
    
    public TestFixture() : this(Path.Combine(string.Empty)) { }
    
    protected TestFixture(string ARelativeTargetProjectParentDir)    
    {
        var LStartupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
        var LContentRoot=GetProjectPath(ARelativeTargetProjectParentDir, 
                        LStartupAssembly);
        var LConfigurationBuilder = new ConfigurationBuilder()            
            .SetBasePath(LContentRoot)                
            .AddJsonFile("appsettings.json")            
            .AddUserSecrets(LStartupAssembly);
            
        var LWebHostBuilder = new WebHostBuilder()            
            .UseContentRoot(LContentRoot)            
            .ConfigureServices(InitializeServices)            
            .UseConfiguration(LConfigurationBuilder.Build())            
            .UseStartup(typeof(TStartup));
           
    FServer = new TestServer(LWebHostBuilder);
    
    FClient = FServer.CreateClient();
    FClient.BaseAddress = newUri("http://localhost:5000");        
    FClient.DefaultRequestHeaders.Accept.Clear();
    FClient.DefaultRequestHeaders.Accept.Add
        (new MediaTypeWithQualityHeaderValue("application/json"));    
}
    
protected virtual voidInitializeServices(IServiceCollection AServices)    
{
    var LStartupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
    var LManager = new ApplicationPartManager        
    {
        ApplicationParts= { new AssemblyPart(LStartupAssembly) },
        FeatureProviders= { new ControllerFeatureProvider(), 
                            new ViewComponentFeatureProvider() }        
    };
    AServices.AddSingleton(LManager);    
}
    
private static stringGetProjectPath
    (string AProjectRelativePath, 
    Assembly AStartupAssembly)    
{
    var LProjectName = AStartupAssembly.GetName().Name;
    var LApplicationBasePath = AppContext.BaseDirectory;
    var LDirectoryInfo = new DirectoryInfo(LApplicationBasePath);
    do        
    {
        LDirectoryInfo=LDirectoryInfo.Parent;
        var LProjectDirectoryInfo = new DirectoryInfo
            (Path.Combine(LDirectoryInfo.FullName, AProjectRelativePath));
        if (LProjectDirectoryInfo.Exists)            
        {
            if (new FileInfo(Path.Combine(LProjectDirectoryInfo.FullName, 
                LProjectName, 
                $"{LProjectName}.csproj")).Exists)                
            {
                return Path.Combine(LProjectDirectoryInfo.FullName, 
                        LProjectName);                
            }            
      }        
}

while (LDirectoryInfo.Parent!=null);
throw new Exception($"Project root could not be located using the application root {LApplicationBasePath}.");    
}
}



Best Practices

It is good to follow given best practices while developing .NET application:

  • Use IDisposable or using keywords to free resources that are not managed.

  • Initialization of members should be deferred if those are not required during the creation of the class object.

  • Collections such as List<T> sets the initial size to 4 elements if no scope is defined during list creation. Furthermore, if we add any part after 4, the collection size is doubled in memory. So, to avoid such a situation and to not take extra memory when a list is not extensive, it is recommended to specify the opening size of the collection.

  • Try to keep the data model simple and well structured. Because if it is too complex, Garbage Collector will take more time in analyzing the whole graph to check which objects can be collected.

  • Make use of yield statements when possible. Yield keyword in C# is used to generate iterator pattern, which is an IEnumerator implementation. The benefit of using a yield statement is that the whole collection does not need to be in memory. It processes one item at a time.

  • Either avoid excessive grouping or aggregate functions in LINQ queries.



Summary

Memory management is critical regardless of the language developer is using to build an application, and even managed languages require at least some basic understanding. This article lays out the basics anyone should be aware of. Still, it would be good to check the MSDN article covering that topic: Memory management and garbage collection (GC) in ASP.NET Core. Also, read the book Pro .NET Memory Management by Konrad Kokosa — arguably the best book on this topic.



Source: Medium


The Tech Platform

0 comments

Comments


bottom of page