C#의 가비지 컬렉터

kj0284
WeKlem
Published in
7 min readJun 23, 2023

--

안녕하세요 위클램의 박경준매니저입니다.

오늘은 C#의 가비지 컬렉터에 대해 알아보겠습니다.

가비지 컬렉터?

가비지 컬렉터를 직역하면 쓰레기 수집가(Garbage Collector)라고 해석되요.

보통 필요가 없는 것들을 쓰레기로 분류하는데요. 컴퓨터의 메모리에서는 어떤 것을 쓰레기로 분류하고 수집할까요?

제가 서든어택과 같은 FPS게임을 한다고 가정했을 때, 총을 쏘면 총알이 나가고, 총알이 장애물이나 적에게 맞았을 때 총알은 “무언가를 맞춘다”라는 자기 할 일을 다 했기 때문에 필요가 없겠죠?

이렇듯 할 일을 다하고 더 이상 쓰이지 않는 기능들이 메모리에 올라와 있는 상태을 가비지(쓰레기)라고 할 수 있어요.

가비지 컬렉터는 Heap이라는 메모리라는 공간에 가비지를 확인 및 제거하고, 메모리 공간을 재배치하는 청소부 역할을 한다고 할 수 있습니다.

컴퓨터의 메모리는 Stack과 Heap이라는 공간이 존재합니다.

Stack은 int, char, double 등으로 만들어진 값들이 저장되고,
Heap은 new 키워드로 만들어진 객체들이 저장되는 공간입니다.

가비지 컬렉터의 작동 원리

가비지 컬렉터는 다음과 같은 원리로 메모리를 청소합니다.

  1. 메모리 할당 :

프로그램의 요소(객체)들이 메모리 위에 올라갈때,
가비지 컬렉터는 요소들의 사이즈에 맞게 메모리 공간에 할당합니다.
(75kb보다 큰 요소들은 LOH라는 메모리 공간에 할당되고, 75kb보다 작은 요소들은 SOH라는 메모리공간에 할당됩니다.)

LOH(Large Object Heap) 와 SOH(Small Object Heap)

앞서 언급한 LOH와 SOH는 메모리의 Heap영역 안에서 분리되어 있습니다.
만들어진 객체가 75kb보다 크면 LOH에, 75kb보다 작으면 SOH에
할당하고 관리됩니다.

2. 사용하는 않는 메모리 추적 :

가비지 컬렉터는 메모리들이 프로그램이 지금 사용하는 메모리인지 확인하고, 사용하고 있지 않은 메모리 공간을 체크합니다.

3. 사용하지 않는 메모리 공간 회수 :

가비지 컬렉터는 체크한 메모리 공간을 회수(할당해제)합니다.

4. 메모리 재배치 :

메모리를 회수한 후 메모리를 재배치하여 빈 공간을 효율적으로 사용할 수 있게 합니다.

가비지 컬렉터가 메모리를 회수 및 재배치 하는 과정

메모리를 재배치 하지 않으면 외부 단편화가 발생할 수 있어요.

외부 단편화의 예

외부 단편화란?

단편화란 “여유 공간이 여러 조각으로 나뉘는 현상”을 의미해요.

위 그림처럼 크기만 봤을 땐 사용 공간이 충분하지만 사용 가능한 공간이 나뉘어져 있어 할당을 할 수 없는 현상을 외부단편화라고 합니다.

가비지 컬렉터는 이러한 과정들을 개발자가 명령하지 않아도 자동으로 수행해줘요.

가비지 컬렉터는 언제 동작될까?

가비지 컬렉터는 다음과 같은 상황에서 동작해요.

할당한 메모리보다 메모리를 더 쓸 때

사용하는 메모리가 일정량 기준치를 넘었을 때 시스템이 사용할 메모리가 부족해지는 것을 방지하기 위해 가비지 컬렉터가 자동으로 동작합니다.

일시 중지 또는 게임 상태 전환

유니티의 게임 실행 중에 게임 상태가 바뀌거나, 게임 일시 정지 등과 같은 상황에서 가비지 컬렉터가 실행되어 더 이상 사용되지 않는 객체들을 정리합니다.

강제로 가비지 컬렉션 호출

개발자가 System.GC.Collect()라는 메서드를 호출하면 강제로 가비지 컬렉션을 실행할 수 있습니다.

가비지 컬렉션을 자주 호출하는 것은 성능 저하가 발생할 수 있기 때문에 자주 호출하지 않도록 유의해야 합니다.

왜 성능저하가 발생할 수 있을까요?
사용하지 않는 메모리를 찾는 과정에서 모든 메모리를 확인합니다.
“모든 메모리를 모두 확인한다”는 것은 시간이 많이 걸리는 작업이에요.

따라서 가비지 컬렉션을 자주 호출하면 성능 저하가 발생할 수 있습니다.

세대별 가비지 컬렉션

유니티는 C#이라는 프로그래밍 언어를 사용하고,
C#은 세대별 가비지 컬렉션이라는 방법을 사용해요.

세대별 가비지 컬렉션이란 가비지 컬렉터가 0세대, 1세대, 2세대 별로 메모리를 나눠서 메모리를 관리하는 방법을 말해요.

세대별 가비지 컬렉션의 원리는 다음과 같아요.

  1. 메모리 할당 단계에서 SOH에 할당된 메모리는 0세대, LOH에 할당된 메모리는 2세대로 정한다.
  2. 가비지 컬렉터가 동작하면 0세대부터 동작하고, 0세대에서 살아남은 메모리는 1세대로, 1세대에서 살아남은 메모리는 2세대로 옮겨진다.
  3. 0세대부터 2세대까지 가비지 컬렉터가 순서대로 동작하는데,
    아래 세대에서 동작 후 메모리 공간이 여유로워지면, 윗 세대를 가비지 컬렉터가 확인하지 않고 종료한다.
  4. 2세대에서 가비지 컬렉터를 동작한 후에 메모리 재배치를 하지 않는다
    (2세대 메모리들은 75kb 이상의 메모리 공간을 사용해서 재배치를 하는 것이 더 비효율적일 수 있기 때문입니다.)

이러한 세대별 가비지 컬렉터는 할당한 메모리보다 메모리를 조금 더 사용했을 때, 낮은 세대의 메모리만 확인하고 가비지 컬렉터가 작동하기 때문에 가비지 컬렉터로 인해 프로그램이 멈추는 시간을 줄여줄 수 있어요.

유니티는 이러한 세대별 가비지 컬렉션 로직을 사용합니다.

유니티의 가비지 컬렉션

가비지 컬렉션 도중에는 프로그램이 멈춘다?

유니티의 가비지 컬렉터는 내부적으로는 세대별 가비지 컬렉터 로직을 사용하고 전체적으로는 Boehm–Demers–Weiser라는 방식을 사용합니다.

이 방식을 쉽게 설명하면 “가비지 컬렉터가 작동하는 동안에는 메인 스레드가 멈춘다”입니다.

정말 짧은 순간에 메인스레드가 멈추는 것이지만, 실시간으로 작동하는 게임의 경우 한순간 성능이 저하되는 성능 스파이크가 발생할 수 있습니다.

이러한 성능 스파이크는 게임의 프레임이 낮아지는 원인이 됩니다.

성능 스파이크?
스파이크는 못이나 가시 같이 뾰족하게 튀어나오는 것을 말해요.

성능 스파이크란 작업량이 단기간 동안 많아져 성능 그래프에서 가시처럼 뾰족하게 튀어나오는 현상을 말해요.

위 그림처럼 부하가 생겨서 뾰족하게 튀어나온 것을 성능 스파이크라고 합니다.

최근 유니티는 이러한 성능 스파이크 발생을 줄이기 위해 점진적 가비지 컬렉션을 옵션으로 제공합니다.

유니티에서 제공하는 점진적 가비지 컬렉션 옵션

점진적 가비지 컬렉션?

점진적 가비지 컬렉션이란 말 그대로 점진적으로 가비지 컬렉터가 작동하는 방법입니다.

즉, 한 번에 가비지 컬렉터를 동작시키는 방법이 이 아닌, 프레임마다 가비지 컬렉터가 일을 조금씩 수행하게 하는 방식입니다.

이 방법은 성능 스파이크 발생을 줄일 수 있다는 장점이 있지만,
평균 게임 프레임이 낮아지는 원인이 될 수 있습니다.

두 방식 모두 장단점이 있기 때문에 어느 방법이 내가 만드는 게임에 맞을지를 판단해서 사용하셔야 합니다.

마무리

오늘은 C# 가비지 컬렉터에 대해서 알아보았습니다.

가비지 컬렉터는 자동으로 메모리를 관리하는 매우 중요한 역할을 하지만 너무 자주 사용하면 게임의 성능이 나빠질 수 있습니다.

가비지 컬렉터의 작동 원리에 대해 이해하고 적절한 타이밍에 가비지 컬렉터가 동작하게 한다면 성능이 더 좋은 게임을 만드는데 도움이 될거에요.

이상으로 포스팅을 마치겠습니다

다들 LGTM(Looks Good To Me)하길 바래요🙂

참고자료

LOH와 SOH 차이 : https://hijuworld.tistory.com/46
세대별 가비지 컬렉션 : https://hijuworld.tistory.com/41
점진적 가비지 컬렉션: https://docs.unity3d.com/kr/2022.1/Manual/performance-incremental-garbage-collection.html
https://blog.naver.com/cdw0424/221573941915
단편화 : https://code-lab1.tistory.com/54
스텍과 힙 : https://glassnabi.tistory.com/8

--

--