Search

GC

class
구조
상태
완료
날짜
목차

GC

C#의 가비지 컬렉션(GC)은 자동으로 메모리 관리를 수행하여 개발자가 직접 메모리를 할당하고 해제하는 데 드는 부담을 줄여줍니다.
C++과 달리 C#은 관리되는 언어이며, 가비지 컬렉터를 통해 사용하지 않는 메모리를 자동으로 회수합니다.
C#과 C++의 차이점을 유니티를 기준으로 설명하겠습니다.

GC의 장단점

C#과 C++은 메모리 관리 방식에서 근본적인 차이를 가집니다. C#은 가비지 컬렉션(GC)을 사용하는 반면, C++은 수동 메모리 관리를 사용합니다.

GC의 장점

메모리 관리의 자동화 : 메모리 누수 및 잘못된 메모리 접근의 위험을 줄이며, 개발자가 메모리 관리에 드는 시간과 노력을 줄일 수 있습니다.
애플리케이션 안정성 향상 : 자동 메모리 관리는 애플리케이션의 안정성을 높이는 데 기여합니다. GC는 사용되지 않는 객체를 정확하게 식별하고 메모리를 안전하게 회수합니다.
모바일 플랫폼에서는 메모리 부족 현상으로 인해 크래시가 발생하지만, GC는 메모리 누수를 방지하여 이 문제를 어느 정도 해결해줍니다.

GC의 단점

성능 오버헤드 : 가비지 컬렉션 프로세스 자체가 CPU 리소스를 사용하므로, 메모리 회수 과정에서 애플리케이션의 성능 저하가 발생할 수 있습니다
메모리 사용량 증가 : GC는 메모리를 즉시 회수하지 않을 수 있으므로, 임시 객체 사용이 많은 애플리케이션에서는 메모리 사용량이 증가할 수 있습니다.

C#의 GC 마크앤 스위프

가비지 컬렉션의 기본 원리는 프로그램이 더 이상 사용하지 않는 메모리를 자동으로 식별하고 회수하는 것입니다.
이 과정은 대체로 두 단계로 진행됩니다.
1.
마킹(Marking) : 가비지 컬렉터가 실행되면, 루트 집합에서 시작하여 접근 가능한 모든 객체를 순회하며 마크합니다. 루트 집합은 스택, 글로벌 변수 등 프로그램이 직접 접근할 수 있는 영역에서 참조하는 객체들의 집합입니다. 이 과정에서 도달할 수 있는 객체는 모두 마킹되며, 도달할 수 없는 객체는 마킹되지 않습니다.
2.
스위핑(Sweeping ) : 마킹 과정이 끝나면, 가비지 컬렉터는 메모리를 순회하면서 마킹되지 않은 객체를 찾아 메모리에서 해제합니다. 이 단계에서 실제로 메모리가 회수됩니다.

GC에 대한 주의사항

GC는 정말 유용한 도구지만, 원칙적으로는 발생시키지 않는 것이 좋습니다.
GC는 관리되지 않은 메모리를 자동으로 회수하기 때문입니다.
기본적으로 C#에서는 객체를 생성하지 않을 수 있을 때, 생성하지 않는 것이 좋습니다.

1. 빈번한 문자열 연산

문자열 연산, 특히 문자열을 빈번하게 결합하거나 수정하는 경우, 매 연산마다 새로운 문자열 객체가 생성되어 가비지를 생성할 수 있습니다.
나쁜 예시
void Update() { string text = "Score: "; for (int i = 0; i < 10; i++) { text += i.ToString(); // 매 반복마다 새로운 문자열 객체 생성 // C#의 string은 문자열의 변경되는 과정에서 객체를 수정하지 않고 새로운 객체를 생성합니다. // 이는 C#이 c++과 다르게 string의 동시성 변경 방조를 위한 것입니다. } }
C#
복사
좋은 예시
void Update() { StringBuilder sb = new StringBuilder("Score: "); for (int i = 0; i < 10; i++) { sb.Append(i); // StringBuilder 사용하여 효율적으로 문자열 결합 } string text = sb.ToString(); }
C#
복사

2. 불필요한 메모리 할당을 포함하는 프레임마다의 호출

매 프레임마다 객체를 생성하는 것은 가비지 컬렉션을 빈번하게 유발할 수 있습니다. 특히, Update() 메소드 내에서 불필요한 메모리 할당이 있는 경우입니다.
나쁜 예시
void Update() { Vector3 newPosition = new Vector3(); // 매 프레임마다 새로운 Vector3 객체 생성 // newPosition을 사용한 로직 처리 }
C#
복사
좋은 예시
private Vector3 newPosition = new Vector3(); void Update() { // 기존에 할당된 newPosition 객체 재사용 // newPosition을 사용한 로직 처리 }
C#
복사

3. 대량의 데이터를 처리할 때 불필요한 할당

대량의 데이터를 처리하는 경우, 예를 들어 리스트나 배열을 다룰 때, 불필요한 할당을 피해야 합니다.
나쁜 예시
void ProcessScores() { List<int> scores = new List<int>(); // 매 호출마다 새로운 리스트 생성 // scores 리스트를 채우는 로직 }
C#
복사
좋은 예시
private List<int> scores = new List<int>(); void ProcessScores() { scores.Clear(); // 기존 리스트 재사용 // scores 리스트를 채우는 로직 } // 추가로 이렇게 관리할 뿐 아니라, Array로 관리해서 기존 scores들을 재 사용하는 방법또한 있습니다. // 위 방법은 여러 string을 관리하는 곳에서 효율적이라 생각합니다. // List에서도 물론 가능합니다.
C#
복사

GC.Alloc이 발생하는 상황

유니티에서 가비지 컬렉션(GC)이 발생하는 시점은 .NET 런타임의 메모리 관리 정책에 따라 결정됩니다.

1. 메모리 할당 임계값 도달

유니티 C# GC는 메모리 할당에 대해 세대별 가비지 컬렉션 모델을 사용합니다.
이 모델에서 객체는 세대 0, 세대 1, 세대 2로 분류됩니다. 세대 0은 가장 최근에 할당된 객체들을 포함하며, 가비지 컬렉션은 주로 이 세대에서 발생합니다. 세대 0의 할당된 메모리가 특정 임계값을 초과하면 가비지 컬렉션이 트리거됩니다.

2. 시스템 메모리 압박

운영 체제가 사용 가능한 메모리가 부족하다고 판단할 경우, 런타임에 가비지 컬렉션을 실행하도록 요청할 수 있습니다. 이는 시스템 전체의 메모리 압박 상황에서 발생합니다.

3. 개발자의 명시적 요청

개발자는 코드 내에서 GC.Collect()를 호출하여 가비지 컬렉션을 강제로 실행할 수 있습니다. 이 방법은 특정 상황에서 유용할 수 있으나, 가비지 컬렉션의 실행 시점을 런타임에 맡기는 것이 일반적으로 더 효율적입니다.

4. 대규모 객체 할당

대규모 객체 힙에 할당되는 큰 객체들(85KB 이상)도 가비지 컬렉션의 트리거가 될 수 있습니다. 세대 2에 속하며, 여기서의 할당은 가비지 컬렉션을 덜 빈번하게 하지만, 발생 시 더 큰 영향을 줄 수 있습니다.

GC.Alloc을 유발하는 상황

유니티에서 프로파일러를 사용하여 분석할 때, GC.Alloc에서 많은 성능이 점유되는 것을 확인할 수 있습니다.
GC.Alloc은 가비지 컬렉터가 관리해야 하는 메모리가 할당되었음을 나타냅니다.
이는 객체가 생성되어 마크 앤 스위프 방식으로 표시되기 때문에, 가비지 컬렉터에 의해 회수되지 않아도 이 부분이 발생합니다.

1. 일반적인 인스턴스 생성

다음과 같이 객체를 생성하면 GC가 발생할 수 있습니다.
MyClass instance = new MyClass();
C#
복사

2. 박싱

값 타입을 참조 타입으로 변환하는 과정에서 메모리 할당이 발생합니다. 이는 특히 값 타입을 object 타입이나 인터페이스로 변환할 때 자주 발생합니다.
int i = 10; object obj = i; // 박싱 발생
C#
복사

3. 문자열 연산

문자열 결합이나 수정 과정에서 새로운 문자열 객체가 생성됩니다. 문자열은 불변 타입이기 때문에, 문자열을 변경하는 모든 작업은 새로운 메모리 할당을 수반합니다.
string fullName = firstName + " " + lastName; // 새로운 문자열 할당
C#
복사

4. 컬렉션 요소 추가

리스트, 딕셔너리 등의 컬렉션에 요소를 추가할 때 내부 배열의 크기를 조정하기 위해 새로운 메모리 할당이 발생할 수 있습니다.
List<int> numbers = new List<int>(); numbers.Add(1); // 컬렉션 내부 배열의 크기 조정 시 할당 발생 가능
C#
복사

5.LINQ

LINQ 쿼리는 종종 내부적으로 새로운 컬렉션을 생성합니다. 이 과정에서 메모리 할당이 발생할 수 있습니다.
var filtered = numbers.Where(n => n > 5).ToList(); // 새로운 리스트 생성
C#
복사

유니티의 점진적 가비지 컬렉션

점진적 가비지 컬렉션의 원리

점진적 가비지 컬렉션은 가비지 컬렉션 작업을 여러 작은 단계로 나누어 실행합니다.
이 방식으로, 각 프레임마다 일정량의 가비지 컬렉션 작업을 수행함으로써, 한 번에 큰 작업을 수행할 때 발생하는 긴 지연 시간을 줄일 수 있습니다.
유니티에서 VSync 나 Application.targetFrameRate에서 Unity는 남은 사용 가능한 프레임 시간을 기준으로 가비지 수집에 할당하는 시간을 조정합니다. 이러한 방식으로 Unity는 대기 시간에 가비지 수집을 실행할 수 있으며 성능에 미치는 영향을 최소화하면서 가비지 수집을 수행할 수 있습니다.

마킹 단계

점진적 마킹 : 이 단계에서는 실행 중인 애플리케이션의 루트에서 도달 가능한 객체를 마킹합니다. 점진적 가비지 컬렉션에서는 이 과정을 여러 프레임에 걸쳐 분산시켜 수행할 수 있으며, 각 프레임에서는 전체 작업의 일부만 수행됩니다.

재귀적 마킹 단계

변경된 참조 추적 : 애플리케이션이 계속해서 실행되면서 객체 간의 참조 관계가 변경될 수 있습니다. 이러한 변경 사항을 추적하고 필요한 재마킹 작업을 수행함으로써, 마킹 과정의 정확성을 보장합니다. 이 작업 역시 점진적으로 수행될 수 있습니다.
이 단계는 상당히 복잡한 과정입니다. 아래에서 이 과정이 어떻게 진행되는지 설명하겠습니다.

스위핑 단계

점진적 스위핑 : 마킹되지 않은 객체, 즉 가비지로 판단된 객체를 메모리에서 해제하는 단계입니다. 점진적 가비지 컬렉션에서는 스위핑 과정도 여러 프레임에 걸쳐 수행될 수 있습니다.

컴팩션 단계 (선택적)

메모리 단편화 감소 : 사용 중인 객체를 메모리 상에서 재배치함으로써 메모리 단편화를 감소시키는 단계입니다. 모든 가비지 컬렉션 사이클에서 컴팩션이 수행되는 것은 아니며, 이 단계의 수행 여부와 방식은 가비지 컬렉터의 구현에 따라 달라질 수 있습니다. 컴팩션 과정은 성능에 상당한 영향을 줄 수 있기 때문에, 점진적으로 수행되는 경우도 있습니다.
이 단계는 선택적으로 표시되었습니다. 이는 GC가 실행되도록 설정되어 있더라도 이 단계가 항상 발생하지는 않기 때문입니다. 런타임은 메모리 상의 단편화 수준을 모니터링하며, 단편화가 특정 임계값을 초과하면 이 단계가 실행됩니다.

재귀적 마킹 단계

재귀적 마킹은 주로 변경된 참조 관계를 대상으로 합니다.
이 과정은 가비지 컬렉션의 마킹 단계가 이미 진행된 후, 애플리케이션 실행 도중에 발생한 참조 상태의 변경을 반영하기 위해 수행됩니다.

변경된 참조 관계의 추적

변경된 참조 관계를 추적하는 방법은 가비지 컬렉터의 구현에 따라 다를 수 있습니다.
유니티에선 쓰기 배리어(write barrier)를 사용하는 것으로 알려 있습니다.
쓰기 배리어는 객체의 참조가 변경될 때 이를 감지하는 메커니즘으로, 변경된 참조를 별도의 리스트에 기록합니다.
가비지 컬렉션의 재귀적 마킹 단계에서는 이 리스트를 사용하여 변경된 참조 관계만을 대상으로 마킹을 수행합니다.