포스트

가비지 컬렉터

가비지 컬렉터는 메모리의 할당과 해제를 관리합니다. 가비지 컬렉터에 대해서 정리했습니다.

가비지 컬렉터

가비지 컬렉터

C#과 같은 현대 객체지향 언어에는 가비지 컬렉터가 존재한다.
가비지 컬렉터는 힙 메모리의 할당, 해제를 관리한다.

가비지 컬렉터는 이름 그대로 쓰레기를 수거하는데 여기서 쓰레기란 ‘더 이상 참조할 수 없게 된 객체‘를 의미한다.

프로그래머가 메모리의 할당과 해제를 신경 쓸 필요가 없다는 점이 장점이다.
하지만 그렇다고 전혀 신경 쓸 필요가 없고 몰라도 되는 것은 아니다.

가비지 컬렉터의 호출 시, 다른 스레드를 일시정지하고 실행하기 때문에 잦은 호출은 프로그램 성능 하락을 가져온다.



가비지의 예시

다음과 같은 상황을 생각해보자.

1
2
3
4
5
6
public void GCTest()
{
    int[] arr = new int[20000];
    for(int i = 0; i < 20000; i++)
        arr[i] = i + 1;
}

GCTest() 내부에서 동적 배열을 할당한다.
반복문을 통해서 이 동적 배열의 각 요소에 값을 할당한다.

그리고 닫는 중괄호를 마주치게 된다. 여기서 문제가 발생한다.
동적 배열에 접근할 수 있는 방법을 잃어버리게 된다.

arr이란 이름은 메서드 내부에서 선언되었기 때문에 외부에서 사용할 수 없다.
즉, 메모리는 해제되지 않았지만 접근할 수 없는 상태다.

이런 상황에서 동적 배열을 가비지라고 부르며 가비지 컬렉터의 수집 대상이 된다.



컬렉팅과 세대

가비지 컬렉터는 효율적인 가비지 컬렉팅을 위해 객체들에 세대를 매긴다.

  • 0세대: 최근에 생성되어 컬렉팅을 아직 겪은 적이 없는 객체
  • 1세대: 컬렉팅을 1번 거쳤지만 해제되지 않은 객체
  • 2세대: 컬렉팅을 2번 거쳤지만 해제되지 않은 객체

세대가 높아질수록 가비지 컬렉팅에서 후순위로 밀려난다.
가비지 컬렉팅을 겪고도 생존한 객체들일수록 중요한 객체라고 판단한다.


가비지 컬렉터는 다음과 같은 기준에 따라서 정책을 달리한다.

  • SOH(Small Of Heap): 가비지 크기가 85kb보다 작은 경우
  • LOH(Large Of Heap): 가비지 크기가 85kb보다 큰 경우

SOH의 경우, 0세대부터 컬렉팅을 시작한다. 컬렉팅을 마친 후의 SOH는 내부 단편화를 없애기 위해 메모리에 재배치된다.
LOH의 경우, 2세대를 포함하여 컬렉팅을 시작한다. LOH는 재배치의 오버헤드도 커, 메모리 재배치가 발생하지 않는다.



가비지 예방

GC의 잦은 호출을 막기 위해서는 주의가 필요하다.


1. 불필요한 리스트의 사용

List<T>는 동적으로 크기를 조절할 수 있는 컨테이너지만, 메모리 효율이 좋지 않다.
일반 배열로 충분하다면 배열을 사용하는 편이 좋다.


2. string 객체의 사용

string 객체의 문자열을 변경하는 작업은 별 게 아닌 것처럼 보인다.
하지만 내부적으로는 C++의 vector와 유사하게 작동한다.

문자 하나가 추가되거나 삭제된다면 문자열 전체가 재할당되는 정책을 가진다.
따라서 자주 변경되는 문자열이라면 string 대신 StringBuilder를 사용할 수도 있다.


3. 오브젝트 풀링

사용할만큼의 객체를 미리 생성해둬서 보관하다가 필요할 때 찾아서 사용한다.
객체를 생성, 제거하는 대신 객체를 활성화, 비활성화하는 방법이다.

객체 생성 및 제거에 필요했던 메모리 할당 및 해제가 없으므로 가비지 컬렉터의 호출은 감소한다.


4. 박싱, 언박싱

박싱은 값 타입을 참조 타입으로 변환하면서 힙에 메모리를 할당하기 때문에 가비지 컬렉터의 관리 대상이 된다.
박싱 및 언박싱은 최소한으로 사용해야 한다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.