.NET Memory Analysis with Linux

For Windows, there are various programs and tools for the analysis of memory/performance problems of .NET programs. These include the “official” programs from Microsoft such as Perfview, Visual Studio or WinDBG.

Under Linux, the choice is unfortunately very limited. Fortunately, Microsoft provides a whole set of tools for analyzing memory/performance problems of .NET programs with the dotnet diagnostic tools. These work under Linux as well as in Windows and with restrictions also under macOS.

dotnet-counters

The first step is to find out if there really is a problem with the managed memory (GC Heap).

This is easily done with dotnet counters. With the help of dotnet counters monitor -p <PROCESS ID> monitoring can be activated for any .NET process.

dotnet counters monitor -p 1234

Press p to pause, r to resume, q to quit.
    Status: Running

[System.Runtime]
    % Time in GC since last GC (%)                                         0
    Allocation Rate (B / 1 sec)                                        8,168
    CPU Usage (%)                                                          0.007
    Exception Count (Count / 1 sec)                                        0
    GC Committed Bytes (MB)                                              321.516
    GC Fragmentation (%)                                                   0.046
    GC Heap Size (MB)                                                  1,074.347

Watch for GC Heap Size (MB). If the value is always increasing but never decreasing, then the application has a memory leak.

dotnet-gcdump

A simple overview of all objects managed by the garbage collector (GC) can be displayed with dotnet gcdump. To do this, first create a memory dump of the desired .NET process with dotnet gcdump collect -p <PROCESS ID>.

dotnet gcdump collect -p 1234

The managed objects can now be viewed with dotnet gcdump report <DUMP FILE>.

dotnet gcdump report 20230906_205437_23141.gcdump

  1,283,668,344  GC Heap bytes
          4,315  GC Heap objects

   Object Bytes     Count  Type
      1,048,600     1,224  System.Byte[] (Bytes > 1M)  [System.Private.CoreLib.dll]
         28,588         1  System.String (Bytes > 10K)  [System.Private.CoreLib.dll]
         16,408         1  MemoryLeak.Lib.LeakData[] (Bytes > 10K)  [MemoryLeak.Lib.dll]
         16,344         1  System.Object[] (Bytes > 10K)  [System.Private.CoreLib.dll]
          8,184         4  System.Object[] (Bytes > 1K)  [System.Private.CoreLib.dll]

The report now shows a list of all objects including their size. The output is about the same as dumpheap -stat from the SOS debugging extension.

At the moment, more information cannot be displayed with dotnet gcdump report. For memory dumps created with dotnet gcdump, the only convenient way at the moment is under Windows with the help of Perfview or Visual Studio.

dotnet-dump

dotnet-dump is the most comprehensive tool for memory analysis of .NET applications. The tool can create memory dumps of .NET processes and provides a virtual environment for analyzing these dumps.

A memory dump of any .NET process is done with dotnet dump collect -p <PROCESS ID>

dotnet dump collect -p 1234

For the analysis of the dump a virtual environment is available afterwards which is started with dotnet dump analyse <DUMP FILE> .

dotnet dump analyze core_20230830_213024

The available SOS commands are documented here and can be displayed with the help command.

An overview of all objects can be displayed with dumpheap -stat. The output is analogous to the dotnet gcdump report <DUMP FILE> used above and shows the number of all managed objects as well as their size in memory.

dumpheap -stat

Statistics:
          MT Count     TotalSize Class Name
7f230ed2e098    18           984 System.String[]
7f230ed56608     7         3,292 System.Char[]
7f230ec4b9d8    14         4,760 System.Int32[]
7f230ebda508     7        36,016 System.Object[]
7f230f1aba78     3        49,224 MemoryLeak.Lib.LeakData[]
7f230ed01038   408        49,754 System.String
7f230f1aac60 2,200        52,800 MemoryLeak.Lib.LeakData
7f230f1ab2a0 2,200        52,800 MemoryLeak.Lib.Data
56490bf28700 2,280        93,408 Free

To get more information about the object of type MemoryLeak.Lib.Data, you can use dumpheap -mt 7f230f1ab2a0 to output each instance. Here in the example there are 2’200 instances of the type MemoryLeak.Lib.Data

dumpheap -mt 7f230f1ab2a0

7ee302816f70     7f230f1ab2a0             24
7ee302816ff0     7f230f1ab2a0             24
7ee302817020     7f230f1ab2a0             24

Now that we have an overview of all objects of the MemoryLeak.Lib.Data type, we can perform further analysis. For example, we can display the “content” of the object. For this we take any object from the list and execute do (dumpobject).

do 7ee302817020

Name:        MemoryLeak.Lib.Data
MethodTable: 00007f230f1ab2a0
EEClass:     00007f230f17be00
Tracked Type: false
Size:        24(0x18) bytes
File:        /home/stef/projects/performance/MemoryLeak/bin/Debug/net7.0/MemoryLeak.Lib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007f230f01fcd0  400000a        8        System.Byte[]  0 instance 00007ee302e000b0 <Buffer>k__BackingField

The object contains only one byte array and is defined as follows:

internal class Data
{
    public byte[]? Buffer { get; set; }
}

Further we can display the used memory of this object with objsize:

objsize 7ee302817020

Objects which 7ee302817020(MemoryLeak.Lib.Data) transitively keep alive:

         Address               MT           Size
    7ee302817020     7f230f1ab2a0             24
    7ee302e000b0     7f230f01fcd0      1,048,600

Statistics:
          MT Count TotalSize Class Name
7f230f1ab2a0     1        24 MemoryLeak.Lib.Data
7f230f01fcd0     1 1,048,600 System.Byte[]
Total 2 objects, 1,048,624 bytes

It happens from time to time that the garbage collector does not free the memory anymore, because an object is still used somehow. Here gcroot helps. With gcroot you can see which objects reference a certain object.

gcroot 7ee302817020

HandleTable:
00007f238af413e8 (strong handle)
-> 7ee300000020 System.Object[]
-> 7ee302816f20 System.Collections.Generic.List<MemoryLeak.Lib.LeakData> (static variable: System.Random._random)
-> 7ee30201e750 MemoryLeak.Lib.LeakData[]
-> 7ee302817008 MemoryLeak.Lib.LeakData
-> 7ee302817020 MemoryLeak.Lib.Data

LLDB

For the analysis of a memory dump, LLDB can also be used instead of dotnet-dump. The advantage of LLDB is that the analysis of managed and native code is possible.

LLDB is probably available in all major linux distributions in the package sources and can be installed with the help of the package manager. Here’s an example of dnf (Fedora):

sudo dnf install lldb

The memory dump is also generated with dotnet dump collect -p <PROCESS ID>. LLDB uses the SOS debugging extension to debug managed code. It may be necessary to install dotnet-sos first so that the SOS debugging extension is found by LLDB.

dotnet tool install --global dotnet-sos
dotnet sos install

The memory dump can then be loaded into the interactive shell of LLDB with lldb –core DUMPFILE

lldb --core core_20231003_100537

Current symbol store settings:
-> Cache: /home/stef/.dotnet/symbolcache
-> Server: https://msdl.microsoft.com/download/symbols/ Timeout: 4 RetryCount: 0
(lldb) target create --core "core_20231003_100537"
Core file '/home/stef/projects/performance/dumps/core_20231003_100537' (x86_64) was loaded.
(lldb)

The memory analysis now works exactly as with dotnet dump. All SOS commands of the SOS debugging extension are available, like dumpheap -stat and so on.

More detailed information for using LLDB can be found in the official documentation from Microsoft.

dotMemory with JetBrains Rider

Of course, there are also commercial products like JetBrains Rider that are available for both Windows and Linux thanks to their platform independence. Since version 2023.2 of Rider you can collect memory dumps and analyze them directly in Rider, just like in the standalone version of dotMemory.