A memory dump can preserve the state of a running .NET application at the moment a memory problem occurs.
Creating the dump is only the first step. The more important task is understanding what the dump tells you.
When a production application uses more memory than expected, developers often start by looking at the total process memory. That number can confirm that something is wrong, but it does not explain why the memory is being used.
A memory dump can help answer more useful questions:
Which object types are using the most memory?
Are large numbers of objects being retained?
Which objects are preventing garbage collection?
Is the managed heap responsible for the memory usage?
Could native memory be contributing to the problem?
Is the application holding data longer than expected?
This article walks through a practical approach to reading a C# memory dump and using it to investigate a production memory problem.
What a Memory Dump Can Tell You
A memory dump is a snapshot of a process.
For a .NET application, it can contain information about the managed runtime, threads, objects, stacks, loaded assemblies, and memory.
A simplified investigation looks like this:
Production Application
|
v
Abnormal Memory Usage
|
v
Memory Dump
|
v
Managed Heap Analysis
|
v
Object Retention Analysis
|
v
Root CauseThe important point is that a dump shows the state of the application at one moment.
It does not automatically tell you what happened before that moment.
That is why application metrics and multiple dumps can be valuable during a longer investigation.
Start With the Application Context
Before opening the dump, record the situation in which it was collected.
For example:
Application: OrderService
Environment: Production
Runtime: .NET
Process ID: 12345
Memory: 7.5 GB
Normal Memory: 2.5 GB
Dump Time: 14:30
Application Version: 5.4.2Also record what users were experiencing.
For example:
- API response times increased
- Process memory kept growing
- Garbage collection became more frequent
- Container memory approached its limitThis context helps you avoid analyzing the dump without understanding why it was collected.
Open the Dump With dotnet-dump
The .NET diagnostics tools provide dotnet-dump for collecting and analyzing .NET process dumps.
A dump can be opened with:
dotnet-dump analyze /path/to/application.dmpAfter opening the dump, you can use diagnostic commands to inspect the process.
For example:
> helpcan be used to view available commands.
The exact commands available can depend on the runtime and diagnostic environment.
First Check the Managed Heap
One of the first useful commands is:
dumpheap -statThis provides a summary of managed object types.
A simplified result might look like:
Count Total Size Type
------------------------------------------------
450000 180 MB System.String
210000 165 MB MyApp.Order
125000 140 MB MyApp.Customer
50000 120 MB System.Byte[]
18000 90 MB MyApp.CacheEntryThis immediately gives you useful information.
You can ask:
Which types have the largest total size?and:
Which types have unexpectedly high counts?These questions are often more useful than simply looking at the total process memory.
Understand Count and Total Size
Consider these two examples.
Example 1 - Many Small Objects
Count: 2,000,000
Total Size: 100 MBThe object count is very high, but the total memory usage is relatively small.
Example 2 - Fewer Large Objects
Count: 10,000
Total Size: 2 GBThe count is much lower, but the memory impact is much larger.
This is why both values matter.
A good first pass should look at:
Object count
+
Total size
+
Object typeFind Specific Object Types
Suppose the heap statistics show:
MyApp.Session
Count: 450000
Total Size: 1.8 GBThat is worth investigating.
You can inspect instances of a specific type using:
dumpheap -type MyApp.SessionThis can help identify the objects currently present in the managed heap.
The next question is not simply whether the objects exist.
The important question is:
Why are these objects still alive?
Understanding Object Retention
The garbage collector removes objects that are no longer reachable.
Consider:
public class CustomerService
{
private readonly List<Customer> _customers = new();
public void Add(Customer customer)
{
_customers.Add(customer);
}
}If CustomerService remains alive for the lifetime of the application and the list continues growing, the Customer objects remain reachable.
The garbage collector cannot remove them simply because the application is not actively using them.
Conceptually:
Long-lived service
|
v
List<Customer>
|
+-- Customer
+-- Customer
+-- Customer
+-- CustomerIf that list keeps growing, memory usage can also keep growing.
This is a common pattern to investigate when a dump shows a large number of long-lived objects.
Find What Is Keeping an Object Alive
A useful diagnostic command for examining an individual object is:
gcroot <object-address>For example:
gcroot 000001F4A1234567The result can show a reference path back to a garbage collection root.
Conceptually, the result might look like:
GC Root
|
v
Singleton Service
|
v
Cache
|
v
List<Customer>
|
v
CustomerThis tells you why the object remains reachable.
The root path is often where the investigation becomes much more interesting.
What Is a GC Root?
A garbage collection root is a reference from a location that can keep an object alive.
Examples can include:
Static fields
Active thread references
Local variables on active stacks
Runtime handles
Long-lived objects
The garbage collector starts from these roots and determines which objects are still reachable.
A simplified model is:
GC Root
|
v
Object A
|
v
Object B
|
v
Object CAs long as the root can reach the object graph, those objects may remain alive.
Static Collections Can Cause Memory Growth
One common pattern is a static collection.
For example:
public static class RequestHistory
{
private static readonly List<string> Requests = new();
public static void Add(string request)
{
Requests.Add(request);
}
}If nothing removes old entries, the collection can grow for the lifetime of the process.
A dump may show:
RequestHistory
|
v
List<string>
|
+-- String
+-- String
+-- String
+-- ...The strings are not necessarily a problem individually.
The problem is the long-lived reference that prevents them from being collected.
Caches Need Special Attention
Caching is another area worth investigating.
A cache is intentionally designed to keep objects in memory.
For example:
private readonly MemoryCache _cache = new(new MemoryCacheOptions());
public void Add(string key, Customer customer)
{
_cache.Set(key, customer);
}Caching can improve performance, but an incorrectly configured cache can consume more memory than expected.
Check:
Cache size
Expiration policy
Entry lifetime
Number of entries
Object size
Eviction behavior
A large cache is not automatically a memory leak.
The important question is whether the memory usage matches the intended cache behavior.
Strings Can Consume Significant Memory
Strings frequently appear near the top of heap statistics.
For example:
System.String
Count: 1,200,000
Total Size: 650 MBThis can happen in applications that process:
Large JSON documents
Logs
HTML
XML
Database results
User-generated content
Do not immediately conclude that strings are the root cause.
Instead, investigate what is creating and retaining them.
For example:
var response = await httpClient.GetStringAsync(request);If a service retrieves very large responses and holds them in memory, the resulting strings can contribute to memory pressure.
Byte Arrays Are Also Important
Another type worth checking is:
System.Byte[]Large byte arrays can appear in:
File processing
Image processing
Network operations
Serialization
Compression
Encryption
Message processing
For example:
var file = await File.ReadAllBytesAsync(path);This loads the complete file into memory.
If several large files are processed concurrently, memory usage can increase significantly.
A streaming approach may be more appropriate when the application does not need the entire file in memory.
The Large Object Heap
.NET has a Large Object Heap, commonly called the LOH.
Large allocations can be placed there instead of the normal small object heap.
Objects on the LOH deserve attention when an application processes large arrays, strings, buffers, or similar objects.
For example:
byte[] buffer = new byte[10_000_000];Repeated large allocations can contribute to memory pressure.
When investigating large objects, determine:
What is being allocated?
Why is it large?
How frequently is it created?
How long does it remain reachable?These questions are more useful than simply identifying that the LOH contains large objects.
Look at Threads When Necessary
Memory problems are not always caused by managed objects.
Threads can also help explain application behavior.
You can inspect thread information using:
threadsA dump may show multiple threads with different states.
For example:
ID State
----------------
1 Running
2 Wait
3 Wait
4 Running
5 WaitIf the application is experiencing both memory and performance problems, thread information can provide additional context.
Managed Memory vs Process Memory
One of the most important concepts in memory troubleshooting is that:
Managed heap size is not always equal to process memory.
A .NET process can use memory outside the managed heap.
Conceptually:
Process Memory
|
+-- Managed Heap
|
+-- Native Memory
|
+-- Runtime Components
|
+-- Loaded Libraries
|
+-- Thread Stacks
|
+-- Other AllocationsTherefore, this situation is possible:
Process Memory: 8 GB
Managed Heap: 3 GBThe remaining memory needs further investigation.
Do not assume that the missing 5 GB represents a garbage collection problem.
Native Memory Can Change the Investigation
Applications can use native memory through:
Native libraries
Interoperability code
Operating system resources
Native buffers
Database drivers
Image libraries
Other unmanaged components
If the managed heap does not explain the process memory usage, investigate outside the managed heap.
This distinction prevents developers from spending hours looking for a managed memory leak that does not exist.
Compare the Dump With Application Metrics
A dump becomes more useful when you know what the application was doing when it was collected.
For example:
Time Process Memory Requests/sec
09:00 2.4 GB 120
10:00 3.1 GB 150
11:00 4.8 GB 175
12:00 7.2 GB 180Now suppose a dump was collected at 12:00.
The investigation can connect:
Increasing traffic
+
Increasing memory
+
Heap statistics
+
Object retentionThis provides much stronger evidence than the dump alone.
Compare Multiple Dumps
If the application remains available, collecting multiple dumps can help identify growth.
For example:
Dump 1 - 3 GB process memory
|
v
Dump 2 - 5 GB process memory
|
v
Dump 3 - 7 GB process memorySuppose the heap statistics show:
MyApp.Session
Dump 1: 100,000 objects
Dump 2: 220,000 objects
Dump 3: 480,000 objectsThat pattern is more interesting than seeing 480,000 objects in a single snapshot.
It suggests that the population is increasing and should be investigated further.
A Practical Investigation Workflow
A production memory investigation can follow these steps.
Step 1 - Confirm the Problem
Check application and infrastructure metrics.
Process memory
GC heap
CPU
Request rate
Error rateStep 2 - Collect the Dump
Capture the application state before restarting the process when possible.
Step 3 - Open the Dump
dotnet-dump analyze application.dmpStep 4 - Inspect Heap Statistics
dumpheap -statLook for unusually large object types.
Step 5 - Investigate Suspicious Types
dumpheap -type MyApp.SessionStep 6 - Find Retention Paths
Select a relevant object and inspect its roots:
gcroot <object-address>Step 7 - Check the Application Code
Search for:
Static collections
Unbounded caches
Long-lived services
Event subscriptions
Large buffers
Background queues
Large request or response objects
Step 8 - Compare With Metrics
Confirm whether the dump findings match the application's runtime behavior.
Step 9 - Collect Another Dump if Needed
A second snapshot can help determine whether an object population is growing.
Step 10 - Fix and Validate
After applying the fix, monitor memory behavior over time.
Example Investigation
Suppose the dump contains:
System.Byte[] 1.2 GB
System.String 800 MB
MyApp.RequestData 650 MB
MyApp.CacheEntry 500 MBThe initial conclusion should not be:
Byte arrays are the problem.Instead, investigate the relationships.
You might discover:
CacheEntry
|
v
RequestData
|
v
Large byte[]Now the investigation has a stronger direction.
The application may be caching large request payloads.
The fix could involve:
Smaller cached values
+
Expiration
+
Size limits
+
StreamingThe correct solution depends on the application's actual requirements.
Common Mistakes
Looking Only at Total Process Memory
Process memory tells you that memory is being used, not necessarily why.
Assuming the Largest Object Type Is the Root Cause
An object type may be large because another component is retaining it.
Ignoring GC Roots
Finding a large object is only the beginning.
The retention path often provides the more important information.
Assuming Every Cache Is a Leak
Caches intentionally retain objects.
Investigate whether their size and lifetime are expected.
Looking Only at Managed Memory
Native memory can account for a significant portion of process memory.
Collecting Only One Dump
A single snapshot cannot show how an object population changes over time.
Ignoring Application Metrics
A dump without runtime context can make diagnosis harder.
Best Practices
Collect Evidence Before Restarting
If the process is still available and the situation permits it, capture the diagnostic state before restarting.
Use Multiple Data Sources
Combine:
Memory dump
+
Application metrics
+
Logs
+
Runtime informationFocus on Retention
Ask why an object remains reachable instead of simply asking why it exists.
Investigate Unexpected Growth
Look for object populations that continue increasing over time.
Treat Dumps as Sensitive Data
Production dumps may contain data that should not be exposed outside approved diagnostic environments.
Validate the Fix
A memory fix is not complete when the code compiles.
Monitor the application after deployment and verify that the memory behavior has changed as expected.
Advantages
Provides a snapshot of the running application's state.
Helps identify object types consuming managed memory.
Can reveal object retention paths.
Allows investigation without relying only on logs.
Multiple dumps can help identify memory growth.
Can help distinguish managed heap issues from other process memory usage.
Disadvantages and Trade-Offs
A dump can be very large.
Analysis may require substantial system resources.
Sensitive information can be present in the dump.
One dump represents only a single point in time.
Native memory problems may require additional diagnostic tools.
Understanding object retention requires familiarity with the application's architecture.
Production Memory Dump Checklist
[ ] Confirm abnormal memory behavior
[ ] Record application and runtime information
[ ] Record current process memory
[ ] Collect the dump before restarting when possible
[ ] Open the dump in a controlled environment
[ ] Run dumpheap -stat
[ ] Identify suspicious object types
[ ] Inspect relevant instances
[ ] Check GC roots
[ ] Investigate caches and static collections
[ ] Check large arrays and strings
[ ] Consider native memory
[ ] Compare with application metrics
[ ] Compare multiple dumps when necessary
[ ] Apply the fix
[ ] Monitor memory after deploymentConclusion
Reading a C# memory dump is not simply about finding the object that consumes the most memory.
The real investigation is about understanding how objects are created, how long they remain alive, and what is keeping them reachable.
Commands such as dumpheap -stat, dumpheap -type, and gcroot can provide useful information about the managed heap and object retention. When these findings are combined with application metrics and, when necessary, multiple dumps, developers can build a much clearer picture of a production memory problem.
The practical approach is:
Memory increase
|
v
Collect dump
|
v
Inspect heap
|
v
Find suspicious objects
|
v
Trace GC roots
|
v
Understand retention
|
v
Fix the application
|
v
Monitor memory againA memory dump does not automatically identify the root cause. It provides evidence. The quality of the investigation depends on connecting that evidence with the application's code, architecture, runtime behavior, and production metrics.
Join the conversation! Your thoughts help the community grow.