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 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.
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):
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 = new Uri("http://localhost:5000");
FClient.DefaultRequestHeaders.Accept.Clear();
FClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
protected virtual void InitializeServices(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 string GetProjectPath(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}.");
}
}
It is good to follow given best practices while developing .NET application:
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.
0 comments