[C#] Garbage Collection

임수정·2024년 7월 10일

📝 Learning Log

목록 보기
32/47
post-thumbnail

📍 Garbage Collecton

일반적으로 프로그램이 실행되면 메모리는 힙(heap)스택(stack) 두 가지 주요영역으로 나뉜다.
은 프로그램 실행 중 동적으로 할당되는 메모리를 관리하며, 스택은 함수 호출과 관련된 지역 변수들을 저장하는 공간이다.

프로그램이 실행되면 메모리 할당이 필요하고, 이 할당된 메모리는 사용이 끝나면 해제되어야한다.
그러나 프로그래머가 명시적으로 메모리를 해제하지 않는 경우가 발생할 수 있고, 그렇게 사용하지 않는 메모리 블록들이 쌓이면 시스템 메모리가 부족해져 성능 저하나 예기치 못한 프로그램 종료를 초래할 수 있다.

가비지 컬렉터는 이러한 사용되지 않는 메모리들을 자동으로 찾아내어 해제하는 기능을 수행한다.

이를통해 프로그래머는 메모리 관리에 대한 부담을 줄일 수 있고, 메모리 누수(memory leak)를 방지할 수 있다. 또한 메모리 관리의 편의성과 안정성을 제공하여 프로그램의 신뢰성을 높이는 데 중요한 역할을 한다.

이러한 garbage colletion을 담당하고 있는 역할을 하고 있는 것을 garbage collector라고 한다.



아직 빈공간이 남아있지만 일정 수준 이상 메모리 공간이 차서 GC가 실행되는 환경이라고 가정

위와 같이 메모리 할당이 이루어진 상태라고 할 때, 메모리 공간 B, E를 참조하는 루트가 없는 상황이다. 이때 B와 E는 Garbage로 취급되고 GC의 대상이 된다.
이러한 상황일때 C#은 다음의 과정으로 Garbage를 처리한다.

  1. Managed Heap의 모든 메모리 영역을 Garbage로 간주하여 초기화
  2. 루트 목록을 돌며 루트가 가리키고 있는 영역과 그렇지 않은 영역을 구분
    이때 할당된 메모리 영역이 다른 영역을 가리킬 수 있으며, 그 영역도 루트와 관련된 영역으로 취급
  3. 루트와 관련 없는 영역만을 Garbage로 남기고, 해당 메모리 영역을 해제(Sweap)
  4. Managed Heap을 순차적으로 탐색하며 해제되어 Empty가 된 영역을 채우도록 앞선 영역을 당겨 연속된 메모리 공간으로 만듦


앞선 과정을 거쳐 B,E 영역이 제거된 Managed Heap은 위와 같다.

이때 GC도 CLR 위에서 실행되는 일종의 소프트웨어이므로 작업할때 오버헤드가 발생한다.
루트 목록을 살피고 Managed Heap을 순회하는 것이 꽤나 큰 시간을 소요하게 될 수 있는데, 다른 어플리케이션의 메모리를 정리하는 것이므로 GC가 쓰레기를 수집하는 동안 해당 어플리케이션이 일시적으로 중지될 수 있다.

CLR(Common Language Runtime)

공용 언어 런타임

마이크로 소프트에서 개발한 소프트웨어 프레임워크의 일부이다. CLR은 .NET 프레임워크의 핵심 컴포넌트로, 다양한 프로그래밍 언어로 작성된 응용 프로그램을 실행하고 관리하는 환경을 제공한다. 주요 기능으로는 자동 메모리 관리, 예외 처리, 스레드 관리, IL(Intermediate Language) 코드 실행 등이 있다.
🔗 CLR 공식 설명

📍 Generation of GC

위의 문제를 개선하기 위해 도입된 것이 Generation 개념

garbage가 나올 가능성이 높은 세대를 우선적으로 수집해주고 조건에 따라 다음 세대를 순차적으로 수집해주는 것이다. 그 기준은 가비지 컬렉션을 겪은 횟수!
가비지 컬렉션을 여러 차례 겪었는데도 메모리에 남아있다면 계속 사용될 가능성이 높은 영역이므로 수집의 후순위로 두고, 그렇지 않은 영역을 먼저 주목하는 것이다. 따라서 할당된지 얼마 안 된 영역일수록 빈번하게 가비지 컬렉션이 일어난다.

처음 메모리를 할당한 영역은 0세대가 되고 GC의 최우선 대상이 된다.


1회의 가비지 컬렉션을 거치고 살아남은 메모리는 1세대로 승격되고, 새롭게 할당된 메모리가 0세대가 된다. 이후 GC는 0세대만 수집을 하게 된다. 0세대의 영역들을 수집해 살아남은 영역은 다음 세대로 승격되고, 다음 세대에 할당된 영역을 초과했을 때 0세대 이후의 영역에 가비지 컬렉션을 해주는 것이다. 전체 Managed Heap을 각 세대에 적절히 분배하고, 해당 영역에 Overflow가 일어났을 때 GC의 대상이 된다.

C#은 0~2세대가 존재하고, 2세대는 최소 2회 이상의 GC의 대상이 되었던 메모리 영역, 1세대는 0세대와 2세대의 사이 과도기에 존재하는 영역, 0세대는 한번도 GC의 대상이 된 적 없는 신생 메모리 영역으로 분류된다.

📖 GC의 효율을 높이는 방법

0세대는 단독으로 가비지 컬렉션이 일어날 수 있지만, 상위 세대는 그럴 수 없다. 2세대 가비지 컬렉션이 일어난다면 반드시 1세대와 0세대도 가비지 컬렉션이 일어난다. 이때 2세대 가비지 컬렉션이 일어나면 모든 세대가 GC의 대상이 되는 것이고, Managed Heap 전체에서 가비지 컬렉션이 일어나는 것이므로 이를 Full GC라고 한다. GC의 핵심은 이 Full GC를 최소화하는 것이다.

따라서 Full GC를 방지하기 위한 주의점은 다음과 같다.

  1. 너무 많은 객체를 할당하지 않는다. 메모리 영역이 가득 차 GC가 애초에 일어나지 않도록 하는것이 가장 좋다.
  2. 매우 큰 객체를 할당하면 LOH(Large Object Heap)이라는 특수 케이스로 분류되어 0세대가 아닌 2세대에 할당된다. 따라서 Full GC가 일어날 확률이 높아지므로 대형 객체 할당을 자제하는 것이 좋다.
  3. 참조 관계를 최소화한다. 참조 관계가 복잡해질수록 힙에 메모리가 남아 있을 확률이 높아지게 되고, GC를 호출할 확률이 높아진다. 또한 GC 이후 메모리 주소를 변경할 때 참조 관계에 있는 주소를 전부 변경해야되는데, 이 과정에 대한 오버헤드도 커진다.
  4. 루트 목록이 작을수록 GC가 빠르게 끝난다.

🔗 GC 부가설명 참조 링크

📖 garbage란?

reference type 변수들을 살펴보면 스택 메모리에 주소 값을 담아두고 힙 메모리에 접근하는 방식으로 저장되어 있다. 여기서 함수가 종료되고 지역 변수들이 제거되면서 스택 메모리에서 힙 메모리를 가리키고 있는 일부 주소값들이 같이 pop된다. 그러면 해당 주소값에 있는 힙 메모리의 데이터에는 더 이상 접근할 수 없는 상태가 된다. 이렇게 참조할 수 없게 된 객체를 garbage라고 부른다.

📍 Garbage Collector가 처리할 수 없는 상황

Cycles (순환 참조) : 객체들이 서로를 참조하는 경우, 이를 해제하기 위해 추가적인 분석이 필요할 수 있다. 몇몇 언어와 실행 환경에서는 순환참조를 검출하고 이를 해결하기 위해 Weak Reference 등의 매커니즘을 제공한다.

Long-lived objects(너무 긴 생존 주기) : 일부 객체들은 프로그램 전반에 걸쳐 오랜 시간 동안 사용될 수 있다. 가비지 컬렉터는 이러한 객체들을 언제 해제할지 판단하기 어려울 수 있다.

Native Resources(네이티브 자원) : C# 외부에서 할당된 네이티브 메모리 리소스나 핸들(예 : 파일 핸들, 네트워크 연결 핸들 등)는 가비지 컬렉션의 대상이 아니므로 직접 해제해 주어야 한다.

Unmanaged Resources(비관리 리소스) : 예를 들어 COM 객체, Windows API 등과 같은 비관리 코드에서 생성된 리소스들은 명시적으로 해제해주어야 한다.

📍 해결 방법

Dispose 매서드 호출 : 'IDisposable' 인터페이스를 구현하여 'Dispose()' 메서드를 제공한다. 이 메서드를 호출하여 리소스를 명시적으로 해제할 수 있다. 일반적으로 'using' 문법을 사용하여 자동으로 'Dispose()' 메서드를 호출하는 것이 좋다.

IDisposable 인터페이스는 C#에서 지원하는 인터페이스이다. 이 인터페이스는 메모리 누수를 방지하고 관리되지 않는 자원을 명시적으로 해제하는데 사용한다. 주로 파일 핸들, 네트워크 연결, 데이터 베이스 연결 등 리소스 해제가 필요한 경우에 사용한다.

가비지 컬렉션은 자동으로 관리되는 메모리 리소스 만을 처리할 수 있으며, IDisposable 인터페이스를 구현한 객체는 Dispose() 메서드를 호출하여 명시적으로 리소스를 해제할 수 있다. 이는 어플리케이션이 사용하는 자원을 효율적으로 관리하고 메모리 누수를 방지하는 데 중요한 역할을 한다.

파괴자(Finalizer) : 리소스를 수동으로 해제할 수 있도록 파괴자를 구현한다. 파괴자는 클래스의 인스턴스가 가비지 컬렉터에 의해 수집될 때 호출되는 메서드이다. 단, 파괴자는 가비지 컬렉션과 관련된 비용이 크기 때문에 사용은 지양해야한다.

SafeHandle 클래스 사용 : 네이티브 리소스를 안전하게 관리할 수 있는 SafeHandle 클래스를 사용하여 자원을 안전하게 해제할 수 있다.

📁 IDisposable 인터페이스의 Dispose()와 SafeHandle 클래스의 Dispose()

  • IDisposable 인터페이스의 Dispose() :개발자가 클래스 내부에서 자원을 직접 해제하는 방법을 구현한다. 왜냐면 인터페이스니까! 일반적으로 using문을 사용하여 Dispose() 메서드가 호출되게 하여 자동으로 자원을 해제할 수 있다.

  • SafeHandle 클래스의 Dispose() : SafeHandle 클래스는 CriticalFinalizerObject를 상속받아 자원 해제에 추가적인 보호 기능을 제공한다. Dispose() 메서드를 호출하여 명시적으로 자원을 해제하거나, Finalize() 메서드를 통해 가비지 컬렉터에 의해 자동으로 자원을 해제할 수 있다. SafeHandle 클래스의 Dispose()가 네이티브 리소스를 관리하는 데 특화되어 있어 보아 안전하고 효율적인 자원관리를 할 수 있도록 설계되어 있다.

profile
언어는 거들 뿐...

0개의 댓글