dotMemory 2023.3 Help

Analyze GC Roots

The retention path of an object always starts with a GC root. From the point of the Garbage Collector, the root is a reference to an object that must not and will not be collected. This makes roots the only possible starting point for building retention graphs. Understanding root types can be essential during the "Who retains the object?" analysis. Sometimes examining retention paths doesn't answer why the object is still in memory. In this case, it makes sense to look at GC roots. For example, a RefCounted handle indicates that some unmanaged COM library retains the object.

There are four possible root types in .NET Framework:

  • Stack references: references to local objects. Such roots live during method execution.

  • Static references: references to static objects. These roots live the entire app domain lifetime.

  • Handles: typically, these are references used for communication between managed and unmanaged code. Such roots must live at least until the unmanaged code needs "managed" objects.

  • Finalizer references: references to objects waiting to be finalized. These roots live until the finalizer is run.

To analyze a root of a retention path, use views that show object retention paths: Group by Similar Retention, Group by Shortest Paths, Key Retention Paths, and Shortest Paths to Roots. Note that all root types distinguished by dotMemory fall into one of the categories mentioned in the list above.

Regular local variable

This is a local variable declared in a method (variable on the stack). Reference to this variable becomes a root during the method lifetime. For example:

static void Main() { ... var collection = new Collection <int>(); ... }
Regular local variable root

Note that in release builds, root's lifetime may be shorter — JIT can discard the variable right after it is no longer needed.

Static reference

When CLR meets a static object (a class member, a variable, or an event), it creates a global instance of this object. The object can be accessed during the entire app lifetime, so static objects are almost never collected. Thus, references to static objects is one of the main root types.

class SomeClass { public static Collection<string> StCollection; }

After the collection is initialized, CLR will create a static instance of the collection. The reference to the instance will exist during the application domain lifetime.

Static reference root

When the static object is referenced through a field, dotMemory shows you field's name.

F-reachable queue / Finalization queue

CLR provides a helpful mechanism for releasing unmanaged resources: the finalization pattern. The System.Object type declares a virtual method Finalize (also called the finalizer) that is called by the Garbage Collector before the object's memory is reclaimed. Typically, you override this method in order to release unmanaged resources. Any object that has a finalizer is put to the Finalization queue (in dotMemory these objects have Finalization Queue root). When a garbage collection takes place, the GC finds such object in the Finalization queue but doesn't run its finalizer directly. Instead, the GC puts the object to the F-reachable queue (the F-Reachable Queue root in dotMemory) and runs the finalizer in a separate Finalization thread (all of these is done for the sake of performance as the finalizer can potentially run any amount of code). On the next GC, the object in the F-reachable queue is garbage collected. The described pattern has drawbacks, and that's why dotMemory offers a special Finalizable objects inspection.

Note that due to the nature of memory profiling, dotMemory always runs a full GC before taking a snapshot. That's why you won't find objects with the Finalization Queue root in snapshots taken via dotMemory. This root type is possible only in raw memory dumps.

F-Queue root

Pinning handle

Interaction of managed and unmanaged code is an additional problem for the Garbage Collector. For example, you need to pass an object from the managed heap to, say, an external API library. As a small object heap is compacted during collection, the object can be moved. This is a problem for the unmanaged code if it relies on the exact object location. One of the solutions is to fix the object in the heap. In this case, GC gets a pinning handle to the object which implies that the object cannot be moved (pinned object). Thus, if you see a Pinning handle root, then probably the object is retained by some unmanaged code. For example, the App object always has a pinned reference.

Pinning handle root

There is one more case when you can see a Pinning handle in a snapshot. Sometimes, it is not possible to correctly identify a static reference: Instead of a Static reference root, you may see an array of objects Object[] retained by the Pinning handle root. This is a true representation of how static references work.

Pinning handle root instead of a static reference root

dotMemory lets you open all pinned objects in a snapshot as a separate object set. To do this, open the Inspections view and in the Heap Fragmentation section, click the pinned objects link.

Pinned objects

Interior local variable

As managed objects can be moved during Garbage Collection (refer to Pinning handle), it is not possible to use native pointers to track their location on the heap. In such a case, interior pointers can be used. The interior pointer declares a pointer to inside a reference type, but not to the object itself. If you see an Interior local variable root that holds an object, then there is probably an interior pointer that points to inside this object. For an example, refer to MSDN.

Interior local variable

RefCounted handle

The root prevents garbage collection if the reference count of the object is a certain value. If an object is passed to a COM library using COM Interop, CLR creates a RefCounted handle to this object. This root is needed as COM is unable to perform garbage collection. Instead, it uses reference counting. If the object is no longer needed, COM sets the count to 0. This means that RefCounted handle is no longer a root and the object can be collected.

Thus, if you see RefCounted handle, then, probably, the object is passed as an argument to unmanaged code.

RefCounted handle root

Weak handle

As opposed to other roots, the Weak handle does not prevent referenced objects from garbage collection. Thus, objects can be collected at any time but still can be accessed by the application. Access to such objects is performed via an intermediate object of the WeakReference type. Such an approach can be efficient when working with temporary data structures like cache. As weak references don't survive full garbage collection, the weak reference handle can come only in combination with other handles. For example, Weak, RefCounted handle.

Weak handle root

Regular handle

When handle type is undefined, dotMemory marks it as Regular handle. Typically, these are references to system objects required during the entire app lifetime. For example, the OutOfMemoryException object. To prevent its collection, the environment references the object through a regular handle.

Regular handle root
Last modified: 11 January 2024