分析 API 中的对象跟踪
垃圾回收将收回死对象占用的内存,并可能压缩释放的空间。 因此,活动对象将被移到堆内。 本主题说明对象移动将如何影响 ObjectID 值,以及如何通过在压缩和非压缩垃圾回收过程中分析 API 来跟踪这些值。
对象移动
移动了对象后,由以前的通知分配的 ObjectID 值将发生更改。 对象本身的内部状态不会发生更改(它对其他对象的引用除外)。 只有对象在内存中的位置会发生更改(因此其 ObjectID 也会更改)。 利用 ICorProfilerCallback::MovedReferences 通知,探查器可以更新其按 ObjectID 跟踪信息的内部表。 MovedReferences 方法的名称可能会令人在某种程度上产生误解,因为此方法甚至会针对未移动的对象发出。
堆中的对象可能有数千乃至数百万个。 通过提供每个对象的前后 ID 来跟踪这么大数量对象的移动是不现实的。 因此,垃圾回收器趋向于移动块中连续的活动对象,这样,这些对象在堆中的新位置中仍然保持连续。 MovedReferences 通知将报告这些连续对象块的前后 ObjectID。
假定某个现有的 ObjectID 值 (oldObjectID) 位于以下范围内:
oldObjectIDRangeStart[i] <= oldObjectID < oldObjectIDRangeStart[i] + cObjectIDRangeLength[i]
在这种情况下,从范围起始处到对象起始处的偏移量如下所示:
oldObjectID - oldObjectRangeStart[i]
对于位于以下范围中的 i 的任何值:
0 <= i < cMovedObjectIDRanges
可以按如下方式计算新的 ObjectID:
newObjectID = newObjectIDRangeStart[i] + (oldObjectID – oldObjectIDRangeStart[i])
当公共语言运行时 (CLR) 挂起时,将进行所有这些回调。 因此,在运行时继续并且发生另一次垃圾回收之前,任何 ObjectID 值都不能更改。
下面的插图显示了垃圾回收之前的 10 个对象。 这些对象的起始地址(相当于 ObjectID)为 08、09、10、12、13、15、16、17、18 和 19。 ObjectID 为 09、13 和 19 的对象为死对象,它们的空间将在垃圾回收过程中被收回。
垃圾回收过程中的对象移动
插图的下半部分显示了垃圾回收后的对象。 已经回收了死对象占用的空间来容纳活动对象。 堆中的活动对象已移到所示的新位置。 因此,它们的 ObjectID 将发生更改。 下表显示了垃圾回收前后的 ObjectID。
对象 |
oldObjectIDRangeStart[] |
newObjectIDRangeStart[] |
---|---|---|
0 |
08 |
07 |
1 |
09 |
|
2 |
10 |
08 |
3 |
12 |
10 |
4 |
13 |
|
5 |
15 |
11 |
6 |
16 |
12 |
7 |
17 |
13 |
8 |
18 |
14 |
9 |
19 |
下表通过指定连续块的起始位置和大小从而精简了信息。 此表准确地演示了 MovedReferences 方法将如何报告信息。
块 |
oldObjectIDRangeStart[] |
newObjectIDRangeStart[] |
cObjectIDRangeLength[] |
---|---|---|---|
0 |
08 |
07 |
1 |
1 |
10 |
08 |
2 |
2 |
15 |
11 |
4 |
检测所有删除的对象
MovedReferences 方法将报告在压缩垃圾回收后仍然存在的所有对象,而不管对象是否已移动。 MovedReferences 未报告的任何对象均已不存在。 但是,并非所有垃圾回收都是压缩垃圾回收。 在 .NET Framework 1.0 和 1.1 版中,探查器未能检测在非压缩垃圾回收(对象完全不会移动的垃圾回收)后仍然存在的对象。 .NET Framework 2.0 版通过以下新方法为此方案提供了更好的支持:
探查器可以调用 ICorProfilerInfo2::GetGenerationBounds 方法来获取垃圾回收堆段的边界。 可以使用生成的 COR_PRF_GC_GENERATION_RANGE 结构中的 rangeLength 字段来确定某个压缩的代中活动对象的范围。
ICorProfilerCallback2::GarbageCollectionStarted 回调指示当前垃圾回收正在回收哪些代。 位于未被回收的某一代中的所有对象在垃圾回收后仍然存在。
ICorProfilerCallback2::SurvivingReferences 回调指示哪些对象在非压缩垃圾回收后仍然存在。
请注意,一个垃圾回收针对一代可以是压缩垃圾回收,而针对另一代可以是非压缩垃圾回收。 也就是说,任何给定的代对于给定的垃圾回收将收到 SurvivingReferences 或 MovedReferences 回调,但不会同时收到两者。
备注
进行垃圾回收后,应用程序将停止,直至运行时完成将有关堆的信息传递到代码探查器的过程。 您可以使用 ICorProfilerInfo::GetClassFromObject 方法来获取对象的类的 ClassID。 您可以使用 ICorProfilerInfo::GetClassIDInfo 或 ICorProfilerInfo2::GetClassIDInfo2 方法来获取有关类的元数据信息。
在 .NET Framework 1.0 和 1.1 版中,当垃圾回收操作完成后,每个仍然存在的对象都应成为根引用、具有作为根引用的父项,或者存在于未回收的代中。 有时可能有不属于任何这些类别的对象。 这些对象要么是由运行时在内部分配的,要么是对委托的弱引用。 在 .NET Framework 1.0 和 1.1 中,分析 API 不允许用户标识这些对象。
在 .NET Framework 2.0 版中,增加了三种方法来帮助探查器准确地阐明何时回收了哪些代,并确定哪些对象是根对象。 这些方法可帮助探查器确定为何对象在回收后仍然存在:
利用 ICorProfilerCallback2::RootReferences2 方法,探查器可以确定通过特殊句柄保持的对象。 通过将 ICorProfilerInfo2::GetGenerationBounds 方法提供的代界限信息与 ICorProfilerCallback2::GarbageCollectionStarted 方法提供的已回收代信息结合使用,探查器能够确定存在于未回收的代中的对象。
参考
ICorProfilerCallback::MovedReferences 方法
ICorProfilerCallback2::SurvivingReferences 方法
ICorProfilerInfo2::GetGenerationBounds 方法