.NET의 가비지 수집 이해


170

아래 코드를 고려하십시오.

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

이제 주 메소드의 변수 c1이 범위를 벗어 났고 GC.Collect()호출 될 때 다른 객체가 더 이상 참조하지 않더라도 왜 끝나지 않습니까?


8
GC는 범위를 벗어난 인스턴스를 즉시 해제하지 않습니다. 필요하다고 생각 될 때 그렇게합니다. 당신은 여기에서 GC에 대한 모든 것을 읽을 수 있습니다 : msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061

@ user1908061 (Pssst. 링크가 깨졌습니다.)
Dragomok

답변:


352

디버거를 사용하고 있기 때문에 여기에 걸려 넘어져서 매우 잘못된 결론을 도출하고 있습니다. 사용자 컴퓨터에서 코드가 실행되는 방식으로 코드를 실행해야합니다. 빌드 + 구성 관리자를 사용하여 릴리스 빌드로 먼저 전환하고 왼쪽 상단 모서리에있는 "액티브 솔루션 구성"콤보를 "릴리스"로 변경하십시오. 다음으로 Tools + Options, Debugging, General로 가서 "Suppress JIT 최적화"옵션을 해제하십시오.

이제 프로그램을 다시 실행하고 소스 코드를 수정하십시오. 여분의 괄호가 전혀 효과가 없는지 확인하십시오. 변수를 null로 설정해도 아무런 차이가 없습니다. 항상 "1"을 인쇄합니다. 이제 원하는대로 작동하고 작동 할 것으로 예상했습니다.

디버그 빌드를 실행할 때 왜 다르게 작동하는지 설명하는 작업이 남습니다. 가비지 콜렉터가 로컬 변수를 발견하는 방법과 디버거를 제공함으로써 그 영향을받는 방법을 설명해야합니다.

우선, 지터 는 메소드의 IL을 기계 코드로 컴파일 할 때 두 가지 중요한 임무를 수행 합니다. 첫 번째는 디버거에서 매우 잘 보입니다. Debug + Windows + Disassembly 창에서 머신 코드를 볼 수 있습니다. 그러나 두 번째 의무는 완전히 보이지 않습니다. 또한 메소드 본문 내의 로컬 변수가 사용되는 방법을 설명하는 테이블을 생성합니다. 이 테이블에는 각 메소드 인수와 두 개의 주소가있는 로컬 변수에 대한 항목이 있습니다. 변수가 객체 참조를 처음 저장할 주소입니다. 그리고 해당 변수가 더 이상 사용되지 않는 머신 코드 명령어의 주소입니다. 또한 해당 변수가 스택 프레임 또는 CPU 레지스터에 저장되어 있는지 여부입니다.

이 테이블은 가비지 콜렉터에 필수적이며 콜렉션을 수행 할 때 오브젝트 참조를 찾을 위치를 알아야합니다. 참조가 GC 힙에있는 객체의 일부인 경우 매우 쉽습니다. 객체 참조가 CPU 레지스터에 저장 될 때 확실히 쉽지 않습니다. 표는 어디를 봐야하는지 알려줍니다.

테이블에서 "더 이상 사용되지 않는"주소는 매우 중요합니다. 가비지 수집기를 매우 효율적으로 만듭니다. 메소드 내에서 사용되고 해당 메소드가 아직 실행을 완료하지 않은 경우에도 오브젝트 참조를 수집 할 수 있습니다. 예를 들어 Main () 메소드는 프로그램이 종료되기 직전에 실행을 중지합니다. 분명히 Main () 메서드 내에서 사용되는 객체 참조가 프로그램 지속 시간 동안 유지되기를 원하지 않을 것입니다. 지터는 테이블을 사용하여 호출하기 전에 Main () 메서드 내에서 프로그램이 진행된 정도에 따라 이러한 로컬 변수가 더 이상 유용하지 않다는 것을 알 수 있습니다.

해당 테이블과 관련된 거의 마술적인 방법은 GC.KeepAlive ()입니다. 그것은이다 매우 그것은 전혀 코드를 생성하지 않으며, 특별한 방법. 유일한 의무는 해당 테이블을 수정하는 것입니다. 그것은 확장로컬 변수의 수명으로 인해 저장된 참조에서 가비지가 수집되지 않습니다. 참조를 수집 할 때 GC가 열망하는 것을 막아야 할 때만 참조가 관리되지 않는 코드로 전달되는 interop 시나리오에서 발생할 수 있습니다. 가비지 콜렉터는 지터에 의해 컴파일되지 않았으므로 해당 코드에서 사용되는 이러한 참조를 볼 수 없으므로 참조를 찾을 위치를 알려주는 테이블이 없습니다. EnumWindows ()와 같이 관리되지 않는 함수에 대리자 개체를 전달하는 것이 GC.KeepAlive ()를 사용해야하는 경우의 대표적인 예입니다.

따라서 릴리스 빌드에서 샘플 스 니펫을 실행 한 후 알 수 있듯이 메소드 실행이 완료되기 전에 로컬 변수 조기에 수집 할 수 있습니다 . 더 강력하게, 메소드가 더 이상 이것을 참조하지 않으면 메소드 중 하나가 실행되는 동안 오브젝트를 수집 할 수 있습니다 . 문제가 있습니다. 그러한 방법을 디버깅하는 것은 매우 어색합니다. 조사 식 창에 변수를 넣거나 검사 할 수 있기 때문입니다. 그리고 GC가 발생하면 디버깅하는 동안 사라질 것 입니다. 그것은 매우 불쾌 할 것이므로 지터는 디버거가 부착되어 있음알고 있습니다. 그런 다음 수정합니다."마지막으로 사용 된"주소를 변경합니다. 그리고 그것을 정상 값에서 메소드의 마지막 명령의 주소로 변경합니다. 메소드가 반환되지 않는 한 변수를 활성 상태로 유지합니다. 메소드가 리턴 될 때까지 계속 볼 수 있습니다.

또한 앞에서 본 내용과 질문 한 이유에 대해서도 설명합니다. GC.Collect 호출이 참조를 수집 할 수 없으므로 "0"을 인쇄합니다. 테이블은 변수가 사용 중임을 말한다 과거의 모든 길 방법의 끝은 GC.Collect를 ()를 호출합니다. 디버거를 연결 하고 디버그 빌드를 실행하여 그렇게 말 하십시오.

변수를 null로 설정하면 GC가 변수를 검사하고 더 이상 참조를 볼 수 없으므로 이제 효과가 있습니다. 그러나 많은 C # 프로그래머가 빠뜨린 함정에 빠지지 않도록하십시오. 실제로 해당 코드를 작성하는 것은 의미가 없습니다. Release 빌드에서 코드를 실행할 때 해당 명령문이 있는지 여부에 관계없이 차이가 없습니다. 실제로 지터 옵티마이 저는 그 효과가 전혀 없으므로 해당 명령문 을 제거 합니다. 따라서 효과가있는 것처럼 보이지만 이와 같은 코드를 작성하지 마십시오 .


이 주제에 대한 마지막 참고 사항은 Office 응용 프로그램으로 무언가를 수행하기 위해 작은 프로그램을 작성하는 문제가있는 프로그래머를 얻는 것입니다. 디버거는 일반적으로 잘못된 경로에서 파일을 가져 오며 요청시 Office 프로그램을 종료하려고합니다. 적절한 방법은 GC.Collect ()를 호출하는 것입니다. 그러나 그들은 앱을 디버깅 할 때 작동하지 않으며 Marshal.ReleaseComObject ()를 호출하여 절대 절대 착륙하지 못하게합니다. 수동 메모리 관리는 보이지 않는 인터페이스 참조를 쉽게 간과하기 때문에 제대로 작동하지 않습니다. GC.Collect ()는 실제로 앱을 디버깅 할 때가 아니라 작동합니다.


1
Hans가 나에게 잘 대답 한 내 질문도 참조하십시오. stackoverflow.com/questions/15561025/…
Dave Nay

1
@ HansPassant 방금이 멋진 설명을 찾았습니다. 여기 또한 내 질문의 일부에 대한 답변입니다 : stackoverflow.com/questions/30529379/… GC 및 스레드 동기화에 대한. 내가 여전히 가지고있는 한 가지 질문 : GC가 실제로 레지스터에 사용되는 주소 (일시 중지 된 동안 메모리에 저장 됨)를 압축하고 업데이트하는지 궁금한가요? 스레드를 일시 중단 한 후 (재개하기 전에) 레지스터를 업데이트하는 프로세스는 OS에 의해 차단 된 심각한 보안 스레드처럼 느껴집니다.
atlaste

간접적으로 그렇습니다. 스레드가 일시 중단되면 GC는 CPU 레지스터의 백업 저장소를 업데이트합니다. 스레드가 다시 실행되면 업데이트 된 레지스터 값을 사용합니다.
Hans Passant

1
@HansPassant, 여기에 설명 된 CLR 가비지 수집기의 분명하지 않은 세부 사항에 대한 참조를 추가하면 감사하겠습니다.
denfromufa

구성이 현명하고 중요한 점은 "코드 최적화"( <Optimize>true</Optimize>in .csproj)가 활성화 된 것입니다. "릴리스"구성의 기본값입니다. 그러나 사용자 정의 구성을 사용하는 경우이 설정이 중요하다는 것을 알고 있어야합니다.
Zero3

34

[단지 내부 마무리 과정에 추가하고 싶었습니다]

따라서 개체를 만들고 개체를 수집 할 때 개체의 Finalize메서드를 호출해야합니다. 그러나이 간단한 가정보다 마무리에 더 많은 것이 있습니다.

짧은 개념 ::

  1. Finalize메소드를 구현하지 않는 객체 , 메모리는 즉시 회수됩니다. 물론
    응용 프로그램 코드로 더 이상 도달 할 수 없다면

  2. Finalize방법, 개념 / 구현을 구현하는 객체 Application Roots,Finalization Queue , Freacheable Queue가 재생되기 전에 온다.

  3. 응용 프로그램 코드로 접근 할 수없는 객체는 쓰레기로 간주됩니다

가정 :: 클래스 / 객체 A, B, D, G, H는 구현하지 않습니다 Finalize Method를 않으며 C, E, F, I, J는 FinalizeMethod를 구현 합니다.

응용 프로그램이 새 객체를 만들면 new 연산자는 힙에서 메모리를 할당합니다. 객체의 유형에 Finalize메서드 가 포함되어 있으면 객체에 대한 포인터가 종료 큐에 배치됩니다 .

따라서 객체 C, E, F, I, J에 대한 포인터가 종료 큐에 추가됩니다. 마무리 큐 가비지 컬렉터에 의해 제어되는 내부 데이터 구조이다. 대기열의 각 항목 은 객체의 메모리를 회수하기 전에 해당 메소드를 호출 해야하는 객체를 가리 킵니다 . 아래 그림은 여러 객체가 포함 된 힙을 보여줍니다. 이러한 객체 중 일부는 응용 프로그램의 루트 에서 접근 할 수 있습니다

Finalize일부는 그렇지 않습니다. 개체 C, E, F, I 및 J가 만들어지면 .Net 프레임 워크는 이러한 개체에 Finalize메서드가 있고 해당 개체에 대한 포인터가 최종 큐에 추가 되었음을 감지합니다 .

여기에 이미지 설명을 입력하십시오

GC가 발생하면 (1 차 수집) 객체 B, E, G, H, I 및 J는 가비지 인 것으로 결정됩니다. A, C, D, F는 위의 노란색 상자에서 화살표로 표시된 응용 프로그램 코드에 의해 여전히 도달 가능합니다.

가비지 콜렉터는 종료 큐를 스캔하여 이러한 오브젝트에 대한 포인터를 찾습니다. 포인터가 발견되면 포인터가 종료 큐에서 제거되고 연결 가능 큐에 추가됩니다 ( "F-reachable"). F- 접근 큐 가비지 컬렉터에 의해 제어되는 다른 내부 데이터 구조이다. 연결 가능 큐의 각 포인터는 해당 를 가질 준비가 된 오브젝트를 식별합니다.

Finalize 메소드를 호출 .

수집 (1 차 수집) 후 관리되는 힙은 아래 그림과 유사합니다. 아래에 주어진 설명 :
1.) 객체 B, G 및 H가 차지한 메모리는 이들 객체에 호출해야 할 finalize 메소드가 없기 때문에 즉시 회수되었습니다 .

2.) 그러나, 메모리는 때문에 E, I 및 J는 재사용 할 수없는 객체에 의해 점유 된 Finalize방법은 아직 호출되지 않았습니다. Frequeable 대기열에 의해 Finalize 메서드 호출이 수행 됩니다.

3.) A, C, D, F는 위의 노란색 상자에서 화살표로 표시된 응용 프로그램 코드에 의해 여전히 도달 가능하므로 어떤 경우에도 수집되지 않습니다.

여기에 이미지 설명을 입력하십시오

Finalize 메서드 호출 전용의 특수 런타임 스레드가 있습니다. 연결 가능 큐가 비어있는 경우 (보통 경우)이 스레드는 휴면 상태입니다. 그러나 항목이 나타나면이 스레드는 깨어나 대기열에서 각 항목을 제거하고 각 개체의 Finalize 메서드를 호출합니다. 가비지 콜렉터는 교정 가능 메모리를 압축하고 특수 런타임 스레드는 각 오브젝트의 메소드를 실행 하여 도달 가능 큐를 비 웁니다 Finalize. 마지막으로 Finalize 메서드가 실행될 때입니다.

다음에 가비지 콜렉터가 호출되면 (2 차 콜렉션), 애플리케이션의 루트가이를 가리 키지 않고 도달 할 수있는 큐 가 더 이상이를 가리 키지 않기 때문에 최종 오브젝트 (실제로 가비지 가 있음)를 볼 수 있습니다. 객체 (E, I, J)의 메모리는 힙에서 간단히 재생됩니다. 아래 그림을 참조하여 바로 위의 그림과 비교하십시오.

여기에 이미지 설명을 입력하십시오

여기서 이해해야 할 중요한 점 은 최종화가 필요한 객체가 사용하는 메모리를 재생하려면 두 개의 GC가 필요하다는 것 입니다. 실제로, 이러한 개체는 이전 세대로 승격 될 수 있으므로 두 개 이상의 컬렉션 캡이 필요합니다.

참고 : F- 접근 큐는 전역 및 정적 변수 뿌리 단지 같은 루트로 간주됩니다. 따라서 오브젝트가 도달 가능한 큐에 있으면 오브젝트에 도달 할 수 있으며 가비지가 아닙니다.

마지막으로, 응용 프로그램 디버깅은 한 가지, 가비지 수집은 다른 것이며 다르게 작동한다는 것을 기억하십시오. 지금까지는 응용 프로그램을 디버깅하는 것만으로 가비지 수집을 감당할 수 없으며 메모리를 조사하려는 경우 여기에서 시작하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.