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:
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: Similar Retention, 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:
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 are one of the main root types.
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.
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 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 an 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. (This 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 is 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 is 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.
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 the 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 can't 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.
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.
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.
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. To get an example, refer to Microsoft Learn.
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.
Weak handle
As opposed to other roots, the Weak handle doesn't 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.
Regular handle
When the handle type is undefined, dotMemory marks it as a 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.