1. C#의 GC란?
C#의 Garbage Collection (GC)은 .NET Framework 및 .NET Core/5+에서 제공되는 메모리 관리 메커니즘으로, 더 이상 사용되지 않는 객체를 자동으로 해제하야 메모리를 회수하고 애플리케이션의 메모리 누수를 방지한다. 이는 CLR (Common Langague Runtime)에서 수행되며, 프로그래머가 메모리를 수동으로 관리할 필요를 줄여준다.
=> Java의 GC와 목표는 동일하다.
2. C# Garbage Collection의 특징
- 자동 메모리 관리
- 프로그래머가 명시적으로 메모리를 해제하지 않아도 GC가 객체의 수명을 추적하고 불필요한 객체를 정리한다.
- 세대 기반 수집 (Generational Garbage Collection)
- GC는 성능을 최적화하기 위해 메모리를 세 가지 세대로 나누어 관리한다.
- Generation 0: 새롭게 생성된 객체. 짧은 수명을 가지는 객체가 주로 여기에 포함된다.
- Generation 1: Generation 0에서 살아남은 객체.
- Generation 2: 장시간 살아남은 객체. 상대적으로 긴 수명을 가지며 GC의 대상이 되는 빈도가 낮다.
- Mark-and-Sweep 알고리즘
- GC는 다음 단계를 통해 메모리를 회수한다.
- Mark: 사용 중인 객체와 그렇지 않은 객체를 식별
- Sweep: 사용되지 않는 객체를 제거하고 메모리를 정리
- Compact (선택적): 메모리 단편화를 방지하기 위해 객체를 연속적으로 배치
- 스레드 안정성
- GC는 애플리케이션 실행 중에도 동작하며, 스레드 안정성을 보장한다. 하지만 높은 부하가 걸리면 GC가 멈춤 현상을 유발할 수 있다.
=> 세대 기반 수집을 통한 개선된 Mark-and-Sweep 알고리즘을 사용한다.
3. Garbage Collection 트리거 조건
- GC는 다음 조건에서 실행될 가능성이 높다:
- 메모리가 부족할 때
- Generation 0의 메모리가 가득 찾을 때
- 애플리케이션이 명시적으로
GC.Collect()를 호출했을 때
- 하지만 이는 권장되지 않는다. GC는 자체적으로 최적화된 시점을 판단해 작동하도록 두는 것이 좋다.
4. 최적화를 위한 팁
IDisposable 구현 및 using 사용
IDisposable 인터페이스를 구현한 객체는 Dispose 메서드를 통해 명시적으로 리소스를 해제할 수 있다.
using 구문을 활용하면 블록 종료 시 리소스가 자동으로 해제된다.
- 큰 객체 관리
- GC는 LOH (Large Object Heap)에서 큰 객체를 관리하며, Generation 2에 속한다. 큰 객체는 가급적 최소화하고 재사용을 고려한다.
- 불필요한 객체 참조 해제
- 필요 없는 객체 참조를
null로 설정하거나 범위를 벗어나게 하여 GC가 수거 대상임을 명확히 한다.
5. Java의 GC와의 비교
공통점
- 자동 메모리 관리
- 두 언어 모두 개발자가 명시적으로 메모리를 해제하지 않아도 불필요한 객체를 자동으로 회수한다.
- Mark-and-Sweep 알고리즘
- 기본적으로 GC는 Mark-and-Sweep 알고리즘을 기반으로 동작하며, 메모리 단편화를 방지하기 위해 일부 경우 압축(compaction)을 수행한다.
- Generational Garbage Collection
- 둘 모두 객체를 세대(Generation)로 나누어 관리하며, 세대를 기반으로 최적화를 수행한다.
- Stop-the-World
- GC가 실행될 때 애플리케이션의 실행을 일시 중지하는 Stop-the-World 이벤트가 발생한다.
- 대규모 애플리케이션에 적합
- 두 언어의 GC는 대규모 애플리케이션에서의 메모리 관리를 위해 설계되었으며, 기본적인 GC 동작은 프로그래머가 신경 쓰지 않아도 효율적으로 작동한다.
차이점
- GC 구현 및 동작 방식
- C#
- GC는 CLR (Common Language Runtime)에서 관리한다.
- 기본적으로 동시성 GC (Concurrent GC)와 백그라운드 GC를 지원하며, 애플리케이션 성능에 따라 GC 전략을 조정한다.
- .NET Core/.Net 5+에서는 Server GC와 Workstation GC 중 선택할 수 있다.
- Server GC: 다중 스레드 환경에 최적화
- Workstation GC: 단일 사용자 환경에 최적화
- Java
- 메모리 구조
- C#
- Managed Heap을 사용하여 객체를 관리하며, 세대별로 나누어 관리한다.
- 큰 객체는 LOH (Large Object Heap)에 별도로 저장되며, Generation 2에 포함된다.
- 메모리 관리가 기본적으로 .NET의 런타임 내부에서 최적화되며, 프로그래머가 수동으로 개입할 일이 적다.
- Java
- JVM은 Young Generation, Old Generation, Metaspace로 메모리를 나눈다.
- Young Generation: 새로운 객체 저장
- Old Generation: 장시간 생존한 객체 저장
- Metaspace: 클래스 메타데이터 저장 (Java 8부터 PermGen 대신 사용)
- 각 영역에 대해 상세한 튜닝 옵션을 제공한다.
- 프로게이머의 개입
- C#
- 프로그래머가
IDisposable 인터페이스를 구현하거나 using 구문을 사용하여 명시적으로 리소스를 정리할 수 있다.
- GC를 강제로 호출하는
GC.Collect()가 있지만 일반적으로 권장되지 않는다.
- 기본적으로 GC 동작을 신뢰하는 방향으로 설계되었다.
- Java
- GC를 강제로 호출할 수 있는
System.gc()가 있지만, JVM이 GC를 최적화된 시점에 실행하도록 두는 것이 좋다.
- Java에서는 GC 동작과 힙 크기를 JVM 옵션으로 세부적으로 조정할 수 있어, 성능 최적화를 위한 개입 여지가 더 많다.
- 성능과 유연성
- C#
- GC는 성능을 중시하며, .NET Core/5+의 Server GC와 Background GC는 다중 프로세서 환경에서 매울 효율적
- 설정이 간단하며, 기본적으로 대부분의 상황에서 최적의 성능을 제공
- Java
- 다양한 GC 구현과 설정 옵션을 제공하며, 실시간 애플리케이션이나 대규모 서버 애플리케이션의 성능 요구에 따라 선택할 수 있다.
- JVM 옵션을 통해 성능과 지연 시간에 대해 세밀한 튜닝이 가능