프로파일링 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를 보여 줍니다.
Object |
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 메서드