가비지 컬렉션 기본 사항

CLR(공용 언어 런타임)에서 GC(가비지 수집기)는 자동 메모리 관리자 역할을 합니다. 가비지 수집기는 애플리케이션의 메모리 할당 및 해제를 관리합니다. 따라서 관리 코드를 사용하는 개발자는 메모리 관리 작업을 수행하기 위해 코드를 작성할 필요가 없습니다. 자동 메모리 관리를 사용하면 실수로 개체 비우기를 수행하지 않거나 메모리 누수를 유발하거나 또는 이미 비워진 개체를 찾기 위해 비워진 메모리에 액세스하려는 경우 등의 일반적인 문제를 해결할 수 있습니다.

이 문서에서는 가비지 수집의 핵심 개념에 대해 설명합니다.

이점

가비지 수집기는 다음과 같은 이점을 제공합니다.

  • 개발자가 수동으로 메모리를 해제할 필요가 없습니다.

  • 관리되는 힙에 효율적으로 개체를 할당합니다.

  • 더 이상 사용되지 않는 개체를 회수하고 이러한 개체의 메모리를 비워 이후 할당에서 이 메모리를 사용할 수 있도록 합니다. 관리되는 개체는 자동으로 시작을 위한 정리된 콘텐츠를 받으므로 개체의 생성자가 모든 데이터 필드를 초기화할 필요가 없습니다.

  • 개체가 다른 개체에 할당된 메모리를 자체적으로 사용할 수 없도록 하여 메모리 안전성을 제공합니다.

메모리 기본 사항

다음 목록은 중요한 CLR 메모리 개념을 요약한 것입니다.

  • 각 프로세스에는 고유한 개별 가상 주소 공간이 있습니다. 동일 컴퓨터의 모든 프로세스는 동일한 실제 메모리와 페이지 파일(있는 경우)을 공유합니다.

  • 기본적으로 32비트 컴퓨터에서는 각 프로세스에 2GB 사용자 모드 가상 주소 공간이 포함됩니다.

  • 애플리케이션 개발자는 가상 주소 공간만 사용하고 실제 메모리는 직접적으로 조작하지 않습니다. 가비지 컬렉션기는 관리되는 힙에서 사용자 대신 가상 메모리를 할당 및 해제합니다.

    네이티브 코드를 작성 중인 경우 Windows 함수를 사용하여 가상 주소 공간을 작업합니다. 이러한 함수는 네이티브 힙에서 사용자 대신 가상 메모리를 할당 및 해제합니다.

  • 가상 메모리는 다음 세 가지 상태일 수 있습니다.

    시스템 상태 설명
    Free 메모리 블록에 가상 메모리에 대한 참조가 없으며, 메모리 블록을 할당에 사용할 수 있습니다.
    예약됨 메모리 블록을 사용자의 작업에 사용할 수 있으며, 다른 할당 요청에는 메모리 블록을 사용할 수 없습니다. 그러나 커밋될 때까지는 이 메모리 블록에 데이터를 저장할 수 없습니다.
    커밋됨 메모리 블록이 실제 스토리지에 할당되어 있습니다.
  • 가상 주소 공간은 조각화될 수 있으며, 이는 주소 공간에 홀이라고 알려진 여유 블록이 있음을 의미합니다. 가상 메모리 할당이 요청된 경우 가상 메모리 관리자는 할당 요청을 만족시킬 수 있도록 충분히 큰 단일 빈 블록을 찾아야 합니다. 2GB의 여유 공간이 있는 경우에도 전체 여유 공간이 한 주소 블록에 있는 경우가 아니면 2GB가 필요한 할당이 실패할 수 있습니다.

  • 예약할 가상 주소 공간이나 커밋할 실제 공간이 충분하지 않은 경우 메모리 부족이 발생할 수 있습니다.

    실제 메모리 압력(실제 메모리 요구량)이 낮은 경우에도 페이지 파일이 사용됩니다. 실제 메모리 압력이 처음으로 높아지면 운영 체제가 데이터를 저장하기 위해 실제 메모리에 공간을 만들어야 하며, 실제 메모리에 있는 데이터 중 일부를 페이지 파일로 백업합니다. 필요할 때까지는 데이터가 페이지 파일로 저장되지 않으므로 실제 메모리 압력이 낮은 상황에서도 페이징이 발생할 수 있습니다.

메모리 할당

사용자가 새 프로세스를 시작하면 런타임에서는 인접한 주소 공간 영역을 이 프로세스에 예약합니다. 이 예약된 주소 공간을 관리되는 힙이라고 합니다. 관리되는 힙에서는 힙에 있는 다음 개체가 할당될 주소의 포인터를 관리합니다. 초기에 이 포인터는 관리되는 힙의 기본 주소로 설정됩니다. 모든 참조 형식은 관리되는 힙에 할당됩니다. 애플리케이션에서 참조 형식을 처음으로 만드는 경우, 이 참조 형식에 대한 메모리는 관리되는 힙의 기본 주소로 할당됩니다. 애플리케이션이 다음 개체를 만들면 런타임은 첫 번째 개체 바로 다음 주소 공간에 해당 개체에 대한 메모리를 할당합니다. 주소 공간을 사용할 수 있는 한 런타임은 이러한 방식으로 새 개체에 대한 공간을 계속 할당합니다.

관리되는 힙에서 메모리를 할당하면 관리되지 않는 힙에서 메모리를 할당하는 것보다 속도가 더 빠릅니다. 런타임에서는 포인터에 값을 더하여 개체에 메모리를 할당하기 때문에, 스택에서 메모리를 할당하는 속도만큼 빠릅니다. 또한 연속으로 할당된 새 개체는 관리되는 힙에 인접하여 저장되므로 애플리케이션에서 개체에 빠른 속도로 액세스할 수 있습니다.

메모리 해제

가비지 수집기의 최적화 엔진은 수행 중인 할당에 따라 수집을 수행하기에 가장 적합한 시간을 결정합니다. 수집을 수행할 때 가비지 수집기는 애플리케이션에서 더 이상 사용되지 않는 개체에 대한 메모리를 해제합니다. 가비지 수집기는 애플리케이션의 루트를 검사하여 더 이상 사용되지 않는 개체를 결정합니다. 애플리케이션 루트에는 정적 필드, 스레드 스택의 지역 변수, CPU 레지스터, GC 핸들, finalize 큐가 포함됩니다. 각 루트는 관리되는 힙에 있는 개체를 참조하거나 Null로 설정됩니다. 가비지 수집기는 나머지 런타임에 이러한 루트를 요청할 수 있습니다. 가비지 수집기는 이 목록을 사용하여 루트에서 접근할 수 있는 모든 개체를 포함하는 그래프를 만듭니다.

그래프에 없는 개체는 애플리케이션 루트에서 연결할 수 없습니다. 가비지 수집기는 연결할 수 없는 개체를 가비지로 간주하고 이 개체에 할당된 메모리를 해제합니다. 수집을 수행할 때 가비지 수집기는 연결할 수 없는 개체에서 사용되는 주소 공간 블록을 찾기 위해 관리되는 힙을 검사합니다. 연결할 수 없는 개체가 발견되면 가비지 수집기는 메모리 복사 기능을 사용하여 메모리에서 연결할 수 있는 개체를 압축합니다. 그러면 연결할 수 없는 개체에 할당된 주소 공간 블록이 해제됩니다. 연결할 수 있는 개체의 메모리가 압축되면 가비지 수집기는 포인터의 위치를 적절하게 수정합니다. 그러면 애플리케이션 루트는 개체의 새 위치를 가리킬 수 있습니다. 또한 가비지 수집기는 관리되는 힙의 포인터 위치를 연결할 수 있는 마지막 개체 다음에 지정합니다.

컬렉션에서 연결할 수 없는 개체의 수가 엄청나게 발견되는 경우에만 메모리를 압축합니다. 수집을 수행한 후에도 관리되는 힙에서 모든 개체가 그대로 남아 있다면 메모리 압축을 수행할 필요가 없습니다.

런타임에서는 성능 향상을 위해 큰 개체의 메모리를 별도의 힙에 할당합니다. 그러면 가비지 수집기는 큰 개체에 할당된 메모리를 자동으로 해제합니다. 하지만 메모리에서 큰 개체가 이동하는 것을 피하기 위해 일반적으로 이 메모리는 압축되지 않습니다.

가비지 수집 조건

가비지 수집은 다음 조건 중 하나가 충족될 경우 발생합니다.

  • 시스템의 실제 메모리가 부족합니다. 메모리 크기는 운영 체제의 메모리 부족 알림이나 호스트에 표시된 메모리 부족으로 감지됩니다.

  • 관리되는 힙의 할당된 개체에 사용되는 메모리가 허용되는 임계값을 초과합니다. 이 임계값은 프로세스가 실행됨에 따라 계속 조정됩니다.

  • GC.Collect 메서드가 호출됩니다. 가비지 수집기가 지속적으로 실행되므로 이 메서드를 호출해야 하는 경우는 거의 없습니다. 이 메서드는 주로 특이한 상황 및 테스트에 사용됩니다.

관리되는 힙

CLR은 가비지 수집기를 초기화한 후 개체를 저장하고 관리하기 위해 메모리 세그먼트를 할당합니다. 이 메모리를 관리되는 힙이라고 하며, 이는 운영 체제의 네이티브 힙과 대조됩니다.

각 관리되는 프로세스마다 관리되는 힙이 있습니다. 프로세스의 모든 스레드는 같은 힙에 개체 메모리를 할당합니다.

메모리를 예약하기 위해 가비지 수집기는 Windows VirtualAlloc 함수를 호출하며 관리되는 애플리케이션을 위해 한 번에 하나의 메모리 세그먼트를 예약합니다. 또한 가비지 수집기는 필요할 때 세그먼트를 예약하고 Windows VirtualFree 함수를 호출하여 (세그먼트에서 개체를 지운 후) 세그먼트를 운영 체제로 돌려보냅니다.

Important

가비지 수집기에서 할당되는 세그먼트 크기는 구현에 따라 다르며 정기적인 업데이트를 포함하여 언제든지 변경될 수 있습니다. 앱에서 특정 세그먼트 크기를 가정하거나 의존해서는 안 되며, 세그먼트 할당에 사용할 수 있는 메모리 크기를 구성하려고 해서도 안 됩니다.

힙에 할당되는 개체의 수가 적을수록 가비지 수집기가 할 일도 줄어듭니다. 개체를 할당할 때는 15바이트만 필요한 상황에서 32바이트 배열을 할당하는 것처럼 필요 이상의 값을 사용하지 마세요.

가비지 수집이 트리거되면 가비지 수집기는 비활성 개체에 의해 점유된 메모리를 회수합니다. 회수 프로세스는 활성 개체를 압축하여 함께 이동하도록 하며, 비활성 공간이 제거되어 힙의 크기가 더 작아집니다. 이 프로세스로써 함께 할당된 개체가 관리되는 힙에서 함께 유지되어 집약성을 계속 유지합니다.

가비지 수집의 개입 수준(빈도와 지속 시간)은 할당 규모 및 관리되는 힙에서 남은 메모리의 크기에 따라 결정됩니다.

힙은 두 힙(대형 개체 힙과 소형 개체 힙)의 누적으로 간주할 수 있습니다. 대형 개체 힙에는 일반적으로 배열인 85,000바이트 이상의 개체가 포함됩니다. 인스턴스 개체가 너무 커지는 경우는 거의 없습니다.

대형 개체 힙에서 사용할 수 있도록 개체의 임계값 크기를 구성할 수 있습니다.

세대

GC 알고리즘은 몇 가지 고려 사항을 기반으로 합니다.

  • 가비지 수집기는 관리되는 전체 힙보다 관리되는 일부 힙에서 더 빠르게 메모리를 압축할 수 있습니다.
  • 개체가 새로울수록 수명은 더 짧아지고 개체가 오래될수록 수명은 더 길어집니다.
  • 새로운 개체일수록 서로 연결되는 경향이 있어서 애플리케이션에서 거의 동시에 액세스됩니다.

가비지 수집은 주로 수명이 짧은 개체의 회수와 함께 발생합니다. 가비지 수집기의 성능을 최적화하기 위해 관리되는 힙은 0세대, 1세대 및 2세대의 3개 세대로 나뉘어 있으므로 수명이 길고 수명이 짧은 개체를 별도로 처리할 수 있습니다. 가비지 수집기는 새 개체를 0세대에 저장합니다. 애플리케이션 수명의 초기에 만들어져 수집 후에도 남아 있는 개체는 수준이 올라가 1세대와 2세대에 저장됩니다. 전체 힙보다 일부 관리되는 힙을 압축하는 것이 더 빠르기 때문에 가비지 수집기는 수집을 수행할 때마다 이 체계를 사용하여 전체 관리되는 힙에서 메모리를 해제하는 대신 특정 세대에서 메모리를 해제할 수 있습니다.

  • 0세대: 이 세대는 가장 어린 세대이며 수명이 짧은 개체를 포함합니다. 수명이 짧은 개체의 예로는 임시 변수가 있습니다. 가비지 수집은 이 세대에서 가장 자주 발생합니다.

    새로 할당된 개체는 새로운 개체 세대를 구성하며 암시적으로 0세대 수집입니다. 그러나 대형 개체인 경우 ‘3세대’라고도 하는 LOH(대형 개체 힙)에서 사용됩니다. 3세대는 물리적 세대로, 논리적으로는 2세대의 일부로 수집됩니다.

    대부분의 개체는 0세대 가비지 수집에서 회수되며 다음 세대까지 남아 있지 않습니다.

    0세대가 꽉 찼을 때 애플리케이션이 새 개체를 만들려고 하면 가비지 수집기는 개체에 대한 주소 공간을 확보하기 위해 컬렉션을 수행합니다. 가비지 수집기는 관리되는 힙에서 모든 개체를 검사하는 대신 먼저 세대 0에서 개체를 검사합니다. 0세대에만 컬렉션을 수행하더라도 애플리케이션에서 새 개체를 계속 만들 수 있는 충분한 메모리를 확보하기도 합니다.

  • 1세대: 이 세대는 수명이 짧은 개체를 포함하며 수명이 짧은 개체와 수명이 긴 개체 사이에서 버퍼 역할을 합니다.

    가비지 수집기에서는 0세대 컬렉션을 수행한 후 연결할 수 있는 개체의 메모리를 압축하고 1세대로 승격시킵니다. 수집 후에도 존재하는 개체는 수명이 긴 성향이 있기 때문에, 이런 개체를 상위 세대로 승격시키는 것이 당연합니다. 가비지 수집기에서 0세대 수집을 수행할 때마다 1세대와 2세대에서 개체를 다시 검사할 필요가 없습니다.

    예를 들어, 0세대에서 수집을 수행했지만 애플리케이션에서 새 개체를 성공적으로 만드는 데 필요한 충분한 메모리를 확보할 수 없는 경우, 가비지 수집기는 1세대에서 2세대 순서로 수집을 수행할 수 있습니다. 또한, 수집이 완료된 후에 세대 1에 존재하는 개체를 세대 2로 승격시킵니다.

  • 2세대: 이 세대에는 수명이 긴 개체가 포함되어 있습니다. 수명이 긴 개체의 예로는 프로세스의 기간 동안 유지되는 정적 데이터가 포함된 서버 애플리케이션의 개체가 있습니다.

    수집이 완료된 후에 2세대에 존재하는 개체는 다음 수집에서 연결할 수 없는 개체로 결정될 때까지 2세대에 보관됩니다.

    대량 개체 힙(‘3세대’라고도 함)의 개체는 2세대에서도 수집됩니다.

가비지 수집은 조건이 충족될 때 특정 세대에서 발생합니다. 하나의 세대를 수집한다는 것은 해당 세대와 그보다 더 젊은 모든 세대의 개체를 수집한다는 것을 의미합니다. 2세대 가비지 수집은 모든 세대의 개체(즉, 관리되는 힙의 모든 개체)를 회수하므로 전체 가비지 수집이라고도 합니다.

유지 및 승격

가비지 수집에서 회수되지 않는 개체는 남은 개체라고 하며 다음 세대로 승격됩니다.

  • 0세대 가비지 수집에서 남은 개체는 1세대로 승격됩니다.
  • 1세대 가비지 수집에서 남은 개체는 2세대로 승격됩니다.
  • 2세대 가비지 수집에서 남은 개체는 2세대에 남아 있습니다.

가비지 수집기는 한 세대의 잔존율이 높음을 탐지하면 해당 세대의 할당 임계값을 늘립니다. 다음 수집으로 상당한 크기의 회수된 메모리가 발생합니다. CLR은 가비지 수집을 지연하여 애플리케이션의 작업 집합이 너무 커지지 않도록 하는 것과 가비지 수집이 너무 자주 실행되지 않도록 하는 두 가지 우선 순위 사이에서 지속적으로 균형을 유지합니다.

임시 세대 및 세그먼트

0세대와 1세대의 개체는 수명이 짧으므로 이러한 세대를 임시 세대라고 합니다.

임시 세대는 임시 세그먼트라는 메모리 세그먼트에 할당됩니다. 가비지 수집기에서 획득하는 새로운 각 세그먼트는 새로운 임시 세그먼트가 되며 0세대 가비지 수집에서 남은 개체를 포함합니다. 이전의 임시 세그먼트는 새로운 2세대 세그먼트가 됩니다.

임시 세그먼트의 크기는 시스템이 32비트 또는 64비트인지, 그리고 실행 중인 가비지 수집기 형식(워크스테이션 또는 서버 GC)에 따라 달라집니다. 다음 표에서는 임시 세그먼트의 기본 크기를 보여 줍니다.

워크스테이션/서버 GC 32비트 64비트
워크스테이션 GC 16MB 256MB
서버 GC 64MB 4GB
논리적 CPU 수가 >4 개의 서버 GC 32MB 2GB
논리적 CPU 수가 >8개의 서버 GC 16MB 1GB

임시 세그먼트에는 2세대 개체가 포함될 수 있습니다. 2세대 개체는 프로세스에 필요하고 메모리가 허용하는 한도만큼 여러 세그먼트를 사용할 수 있습니다.

임시 가비지 수집에서 해제된 메모리의 크기는 임시 세그먼트의 크기로 제한됩니다. 해제되는 메모리의 크기는 비활성 개체가 점유했던 공간에 비례합니다.

가비지 수집 중 수행되는 작업

가비지 수집은 다음 단계로 구성됩니다.

  • 모든 활성 개체를 찾아 목록을 만드는 표시 단계

  • 압축될 개체에 대한 참조를 업데이트하는 재배치 단계

  • 비활성 개체에 의해 점유된 공간을 회수하고 남은 개체를 압축하는 압축 단계. 압축 단계에서는 가비지 수집에서 남은 개체가 세그먼트의 오래된 쪽으로 이동됩니다.

    2세대 수집은 여러 세그먼트를 점유할 수 있으므로 2세대로 승격된 개체는 오래된 세그먼트로 이동될 수 있습니다. 1세대 및 2세대 남은 개체는 2세대로 승격되므로 모두 다른 세그먼트로 이동될 수 있습니다.

    일반적으로 대형 개체를 복사하면 성능 저하가 발생하기 때문에 LOH(대형 개체 힙)는 압축되지 않습니다. 하지만 .NET Core 및 .NET Framework 4.5.1 이상에서, GCSettings.LargeObjectHeapCompactionMode 속성을 사용하면 필요 시 대형 개체 힙을 압축시킬 수 있습니다. 또한 다음 중 하나를 지정하여 하드 한도가 설정된 경우 LOH가 자동으로 압축됩니다.

가비지 수집기는 다음 정보를 사용하여 개체가 활성 개체인지 여부를 판단합니다.

  • 스택 루트: JIT(Just-In-Time) 컴파일러 및 스택 워커에서 제공되는 스택 변수입니다. JIT 최적화는 가비지 수집기로 보고되는 스택 변수 내에서 코드 영역을 늘리거나 줄일 수 있습니다.

  • 가비지 수집 핸들: 관리되는 개체를 가리키고 사용자 코드나 공용 언어 런타임에 의해 할당될 수 있는 핸들입니다.

  • 정적 데이터: 다른 개체를 참조할 수 있는 애플리케이션 도메인의 정적 개체입니다. 각 애플리케이션 도메인은 해당 정적 개체를 추적합니다.

가비지 수집이 시작되기 전에 가비지 수집을 트리거한 스레드를 제외한 모든 관리되는 스레드가 일시 중단됩니다.

다음 그림에서는 가비지 수집을 트리거하여 다른 스레드가 일시 중단되도록 하는 스레드를 보여 줍니다.

Screenshot of how a thread triggers a Garbage Collection.

관리되지 않는 리소스

애플리케이션이 만드는 대부분의 개체에 대해 가비지 수집을 사용하여 필요한 메모리 관리 작업을 자동으로 수행할 수 있습니다. 하지만 관리되지 않는 리소스의 경우는 명시적으로 정리할 필요가 있습니다. 가장 일반적인 형태의 관리되지 않는 리소스로는 파일 핸들, 창 핸들 또는 네트워크 연결 등의 운영 체제 리소스를 래핑하는 개체를 들 수 있습니다. 가비지 수집기에서는 관리되지 않는 리소스를 캡슐화하는 데 사용되는 관리되는 개체의 수명을 추적할 수 있지만, 리소스 정리 방법에 대한 구체적인 정보는 알 수 없습니다.

관리되지 않는 리소스를 캡슐화하는 개체를 정의하는 경우 관리되지 않는 리소스를 정리하는 데 필요한 코드를 공용 Dispose 메서드에 제공하는 것이 좋습니다. Dispose 메서드를 제공하면 개체 사용자가 개체 작업을 마쳤을 때 명시적으로 리소스를 해제할 수 있습니다. 관리되지 않는 리소스를 캡슐화하는 개체를 사용하는 경우 필요에 따라 Dispose를 호출해야 합니다.

형식의 소비자가 실수로 Dispose를 호출하지 않은 경우 관리되지 않는 리소스를 해제하는 방법도 제공해야 합니다. 안전한 핸들을 사용하여 관리되지 않는 리소스를 래핑하거나 Object.Finalize() 메서드를 재정의할 수 있습니다.

관리되지 않는 리소스 정리에 대한 자세한 내용은 관리되지 않는 리소스 정리를 참조하세요.

참고 항목