分析 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])
所有這些回呼都是在 Common Language Runtime (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 方法