Object Tracking in the Profiling API
Garbage collection reclaims the memory occupied by dead objects and may compact the freed space. As a result, live objects are moved within the heap. This topic explains how object movement affects ObjectID values, and how these values are tracked by the profiling API during compacting and non-compacting garbage collection.
When objects are moved, ObjectID values that were assigned by previous notifications change. The internal state of the object itself does not change, except for its references to other objects. Only the object's location in memory (and therefore its ObjectID) changes. The ICorProfilerCallback::MovedReferences notification lets a profiler update its internal tables that track information by ObjectID. The MovedReferences method name is somewhat misleading, because it is issued even for objects that were not moved.
The number of objects in the heap can number thousands or millions. It would be impractical to track the movement of such a large number of objects by providing a before and after ID for each object. Therefore, the garbage collector tends to move contiguous live objects in blocks, so they stay contiguous in their new locations in the heap. The MovedReferences notification reports the before and after ObjectIDs of these contiguous blocks of objects.
Assume that an existing ObjectID value (oldObjectID) lies within the following range:
oldObjectIDRangeStart[i] <= oldObjectID < oldObjectIDRangeStart[i] + cObjectIDRangeLength[i]
In this case, the offset from the start of the range to the start of the object is as follows:
oldObjectID - oldObjectRangeStart[i]
For any value of i that is in the following range:
0 <= i < cMovedObjectIDRanges
you can calculate the new ObjectID as follows:
newObjectID = newObjectIDRangeStart[i] + (oldObjectID – oldObjectIDRangeStart[i])
All these callbacks are made while the common language runtime (CLR) is suspended. Therefore, none of the ObjectID values can change until the runtime resumes and another garbage collection occurs.
The following illustration shows 10 objects before garbage collection. Their start addresses (equivalent to ObjectIDs) are 08, 09, 10, 12, 13, 15, 16, 17, 18, and 19. The objects with ObjectIDs 09, 13, and 19 are dead, and their space will be reclaimed during garbage collection.
The lower part of the illustration shows the objects after garbage collection. The space that was occupied by the dead objects has been reclaimed to hold live objects. The live objects in the heap have been moved to the new locations that are shown. As a result, their ObjectIDs will change. The following table shows the ObjectIDs before and after garbage collection.
The following table compacts the information by specifying the starting positions and sizes of contiguous blocks. This table shows exactly how the MovedReferences method reports the information.
The MovedReferences method reports all objects that survive a compacting garbage collection, regardless of whether they moved. Any object not reported by MovedReferences did not survive. However, not all garbage collections are compacting. In the .NET Framework versions 1.0 and 1.1, the profiler could not detect objects that survived a non-compacting garbage collection (a garbage collection in which no objects get moved at all). The .NET Framework version 2.0 provides better support for this scenario through the following new methods:
The profiler may call the ICorProfilerInfo2::GetGenerationBounds method to get the boundaries of the garbage collection heap segments. The rangeLength field in the resulting COR_PRF_GC_GENERATION_RANGE structure can be used to determine the extent of live objects in a compacted generation.
The ICorProfilerCallback2::GarbageCollectionStarted callback indicates which generations are being collected by the current garbage collection. All objects that are in a generation that is not being collected will survive the garbage collection.
The ICorProfilerCallback2::SurvivingReferences callback indicates which objects survived a non-compacting garbage collection.
Note that a single garbage collection may be compacting for one generation and non-compacting for another generation. That is, any given generation will receive either SurvivingReferences or MovedReferences callbacks for a given garbage collection, but not both.
After a garbage collection, an application is stopped until the runtime has finished passing information about the heap to the code profiler. You can use the ICorProfilerInfo::GetClassFromObject method to obtain the ClassID of the object's class. You can use the ICorProfilerInfo::GetClassIDInfo or ICorProfilerInfo2::GetClassIDInfo2 method to obtain metadata information about the class.
In the .NET Framework versions 1.0 and 1.1, when a garbage collection operation is complete, every surviving object is expected to be a root reference, to have a parent that is a root reference, or to exist in a generation that was not collected. At times, it is possible to have objects that do not belong to any of these categories. These objects are either allocated internally by the runtime or are weak references to delegates. In the .NET Framework 1.0 and 1.1, the profiling API does not let the user identify these objects.
In the .NET Framework version 2.0, three methods were added to help the profiler clarify exactly which generations are collected and when, and to identify which objects are roots. These methods help the profiler determine why objects remain after a collection:
The ICorProfilerCallback2::RootReferences2 method enables the profiler to identify objects that are held through special handles. The generation bounds information supplied by the ICorProfilerInfo2::GetGenerationBounds method combined with the collected generation information supplied by the ICorProfilerCallback2::GarbageCollectionStarted method enable the profiler to identify objects that exist in generations that were not collected.