.NET Memory Management

Tom Kandula
6 min readApr 18, 2021

The basics you should know

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.

Implementation details

We already said that value types (passed by value) are allocated on the Stack and reference types (passed by reference) are allocated on the Heap. That would be the typical scenario. However, this is not always the case. Let us rephrase it.

Value types might be allocated on the Stack but also might be allocated on the Heap (i.e. boxed values) or might be allocated in a special statics blob. Reference types are usually allocated on the Heap but sometimes on the Stack by object stack allocation technique. An important note is that both value types and reference types might be allocated even on CPU registers.

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 a using 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 Server { get; }
public HttpClient Client { get; }

public void Dispose()
{
Client.Dispose();
Server.Dispose();
}

public TestFixture() : this(Path.Combine(string.Empty)) { }

protected TestFixture(string relativeTargetProjectParentDir)
{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
var contentRoot = GetProjectPath(relativeTargetProjectParentDir, startupAssembly);
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(contentRoot)
.AddJsonFile("appsettings.json")
.AddUserSecrets(startupAssembly);

var webHostBuilder = new WebHostBuilder()
.UseContentRoot(contentRoot)
.ConfigureServices(InitializeServices)
.UseConfiguration(configurationBuilder.Build())
.UseStartup(typeof(TStartup));

Server = new TestServer(webHostBuilder);

Client = Server.CreateClient();
Client.BaseAddress = new Uri("http://localhost:5000");
Client.DefaultRequestHeaders.Accept.Clear();
Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

protected virtual void InitializeServices(IServiceCollection services)
{
var startupAssembly = typeof(TStartup).GetTypeInfo().Assembly;
var applicationPartManager = new ApplicationPartManager
{
ApplicationParts = { new AssemblyPart(startupAssembly) },
FeatureProviders = { new ControllerFeatureProvider(), new ViewComponentFeatureProvider() }
};
services.AddSingleton(applicationPartManager);
}

private static string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
{
var projectName = startupAssembly.GetName().Name;
var applicationBasePath = AppContext.BaseDirectory;
var directoryInfo = new DirectoryInfo(applicationBasePath);
do
{
directoryInfo = directoryInfo.Parent;
var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));
if (projectDirectoryInfo.Exists)
{
if (new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj")).Exists)
{
return Path.Combine(projectDirectoryInfo.FullName, projectName);
}
}
}
while (directoryInfo.Parent != null);
throw new Exception($"Project root could not be located using the application root {applicationBasePath}.");
}
}

Best practises

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.

An example of an entire class (for better context view) using the yield contextual keyword is presented below. More here: yield (C# Reference).

public class GetRoomsInfoQueryHandler : TemplateHandler<GetRoomsInfoQuery, IEnumerable<GetRoomsInfoQueryResult>>
{
private const string PluralSuffix = "s";
private readonly DatabaseContext _databaseContext;

public GetRoomsInfoQueryHandler(DatabaseContext databaseContext) => _databaseContext = databaseContext;

public override async Task<IEnumerable<GetRoomsInfoQueryResult>> Handle(GetRoomsInfoQuery request, CancellationToken cancellationToken)
{
var queryResults =
from rooms in _databaseContext.Rooms
group rooms by rooms.Bedrooms
into grouping
select new QueryRoomsInfoDto
{
Bedrooms = grouping.Key,
TotalRooms = grouping.Select(rooms => rooms.Bedrooms).Count()
};

return await Task.FromResult(GetRoomsInfo(queryResults));
}

private static IEnumerable<GetRoomsInfoQueryResult> GetRoomsInfo(IEnumerable<QueryRoomsInfoDto> queryResults)
{
foreach (var queryResult in queryResults)
{
var bedroomSuffix = queryResult.Bedrooms > 1 ? PluralSuffix : string.Empty;
var roomSuffix = queryResult.TotalRooms > 1 ? PluralSuffix : string.Empty;
yield return new GetRoomsInfoQueryResult
{
Id = Guid.NewGuid(),
Info = $"{queryResult.TotalRooms} room{roomSuffix} with {queryResult.Bedrooms} bedroom{bedroomSuffix}."
};
}
}
}

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.

Thank you for reading this article! Please leave a comment should you have any questions or have a different experience.

--

--

Tom Kandula

Software Engineer | NET | Azure | React | Freelancer & Contractor