본문 바로가기

C#/이것이 C#이다

[ 이것이 C#이다 ] Chapter 22. 가비지 컬렉션

[ 학습 흐름 ]
  1. 가비지 컬렉터
  2. 세대별 가비지 컬렉션

[ 가비지 컬렉터 ]

1 . C / C++의 문제

[ 메모리 해제 ]

- C++에서는 객체를 할당하기 위해 일일이 메모리 공간을 확보해야 한다 

- 객체를 할당한 후에는 힙을 가리키는 포인터를 유지하다 , 객체를 다 사용하면 해당 포인터가

가리키는 메모리를 해제해줘야 한다 . 

- 객체 메모리의 해제는 깜빡 하기 쉬운 문제이다 .


[ 해제된 포인터에 접근 ]

- 메모리를 해제후 , 해당 포인터에 접근해서 코드를 실행하는 문제도 있다.

- 해당 포인터가 가리키는 메모리에 엉뚱한 코드가 있을 수도 있다 .


[ 비싼 비용 ]

- 힙에 객체를 할당하기 위해서는 비싼 비용을 치뤄야 한다 .

- C / C++ 기반의 프로그램을 실행하는 C - 런타임은 객체를 담기 위한 메모리를 여러개의 블록으로 나누고 ,

블록을 링크드 리스트로 묶어 관리한다 

- 객체를 힙에 할당하는 코드 실행시 , C - 런타임은 링크드 리스트를 순차적으로 탐색하여 , 해당 객체를 담을 여유가 있는

메모리 블록을 찾는다 .

- 적당한 크기의 메모리 블록을 만나면 이 메모리 블록을 쪼개서 객체를 할당 ,메모리 블록의 링크드 리스트를 재조정 한다 

- 즉 , 단순히 메모리 공간에 데이터를 집어넣는게 아닌 , 공간을 탐색 / 분할/ 재조정하는 오버헤드가 필요하다 .


2 . CLR의 자동 메모리 관리 - 가비지 컬렉션

[ 가비지 컬렉션 ]

- CLR의 자동 메모리 관리 기능의 중심에는 가비지 컬렉션이 있다 .

- 가비지 컬렉션은 프로그래머로 하여금 컴퓨터가 무한한 메모리를 가진 것처럼 간주 , 코드를 작성할 수 있게 한다 .


[ 가비지 컬렉터 ]

- 작성한 프로그램이 객체를 할당해서 일을 하고 있으면 , 가비지 컬렉터는 객체중 쓰레기인것과 아닌것을 완벽히 분리 , 쓰레기들만 수거해간다 .

- 가비지 컬렉터 역시 소프트웨어이기에 CPU와 메모리 같은 컴퓨팅 자원을 소모한다 .

- 작성한 코드도 사용해야 하는 자원을 가비지 컬렉터도 같이 사용한다는 의미이다 .

- 가비지 컬렉터가 최소한으로 자원을 사용한다면 , 프로그램의 성능을 아낀 자원만큼 끌어 올릴 수 있다 .


[ 가비지 컬렉터가 치우지 못하는 메모리 ]

- 기본적으로 C#으로 작성된 모든 코드는 관리형 코드에 속한다 .

- CLR은 실행되는 코드에 대해 메모리 할당 ,보안,스레딩 등의 임무와 함께 가비지 컬렉션도 담당한다 .

- unsafe 키워드를 이용하면 비관리형 코드를 작성 할 수 있다 .

- 이 경우 CLR이 제공하는 서비스는 받을 수 없다 .


[ 가비지 컬렉터를 효율적으로 사용하기 ]

- 프로그래머는 가비지 컬렉터가 어떻게 동작하는지에 대한 메커니즘을 이해하고 있어야 한다 .

- 해당 이해를 바탕으로 코딩 지침을 세우고 , 지침을 바탕으로 코드를 작성해야 한다 .


[ 개처럼 할당하고 정승처럼 수거하라 ]

1 . CLR이 객체를 메모리에 할당하는 방법

[ 객체 만들기 ]

object A = new  object();

- C#으로 작성한 소스코드를 컴파일해서 실행 파일을 만들고 , 실행하면

CLR은 프로그램을 위한 일정 크기의 메모리를 확보한다 .

- C-런타임처럼 메모리를 쪼개지는 않고 넓은 메모리 공간을 통째로 확보해서 하나의 Managed Heap을 마련한다 .

- CLR은 Managed Heap 메모리의 첫번째 주소에 다음 객체를 할당할 메모리의 포인터를 위치시킨다 .

- Managed Heap에 첫번째 객체를 할당하면 다음 객체를 할당할 메모리의 포인터가 가리키는 주소에 A 객체를할당 , 

포인터를 A객체가 차지하는 공간 바로 뒤로 이동시킨다 .


[ 두번째 객체 만들기 ]

object B = new  object();

- 두 번째로 만드는 객체는 첫번째 객체의 바로 뒤 , 즉 다음 객체를 할당할 메모리의 포인터가 가리키는 곳에 할당된다 .

- CLR은 객체가 위치할 메모리를 할당하기 위해 메모리 공간을 쪼개 만든 링크드 리스트를 탐색하는 시간도 소요하지 

않고 ,  그 공간을 다시 나눠 리스트를 재조정하는 작업이 필요 없다.

- 그저 메모리에 할당하는 것이 전부기에 C- 런타임에 비해 CLR의 객체 할당 매커니즘은 단순하고 효율적이다 .


2 . 객체가 메모리에서 해제되는 시점

[ 수거 ]

if(true)
{
	object a = new  object();
}

- 값 형식의 객체는 스택에 할당되었다 , 태어난 코드블록이 끝나면 메모리로부터 바로 사라지고 , 참조 형식 객체들만

힙에 할당되어 코드 블록과 관계없이 계속 살아남는다 .

- 위 코드에서 실제 객체의 내용물A는 힙에 할당되어 있지만 , a는 객체 A가 위치하고 있는 힙 메모리의 주소를 참조하고 있을 뿐이다 .

- 객체 A가 위치하고 있는 메모리를 참조하는 a는 코드 블록이 끝나면 스택에서 사라진다 .

- 객체 A는 코드 어디에서도 접근 못하기에 가비지 컬렉터가 수거하게 된다 .


[ 루트 ]

- 위 예시의 a처럼 할당된 메모리의 위치를 참조하는 객체를 루트라고 한다 .

- 루트의 경우 스택에 생성될수도 , 정적 필드처럼 힙에 생성될수도 있다 .

- .NET 애플리케이션이 실행되면 , JIT 컴파일러가 이 루트들을 목록으로 만들고 CLR은 이 루트 목록을 관리하며 상태를 갱신한다 .

- 루트가 중요한 이유는 가비지 컬렉터가 CLR이 관리하던 루트 목록을 참조해서 쓰레기 수집을 하기때문이다 .


[ 가비지 컬렉터가 루트 목록을 이용 , 쓰레기를 정리하는 과정 ]

  1. 가비지 컬렉터는 모든 객체를 쓰레기라 가정한다 . 즉 ,루트 목록내 어떤 루트도 메모리를 가리키지 않는다 가정한다 .
  2. 루트 목록을 순회 , 각 루트가 참조하고 있는 힙 객체와의 관계 여부를 조사한다 . 만약 루트가 참조하고 있는 힙의 객체가 또다른 힙 객체를 참조하고 있다면 이 역시 해당 루트와 관계가 있는 것으로 판단한다 .
  3. 어떤 루트와도 관계가 없는 힙의 객체들은 쓰레기로 간주된다 .
  4. 쓰레기 객체가 차지하고 있던 메모리는 이제 비어있는 공간이다 .
  5. 루트 목록에 대한 조사가 끝나면 , 가비지 컬렉터는 힙을 순회하면서 쓰레기가 차지하던 비어 있는 공간에 쓰레기의 인접 객체들을 이동시켜 차곡 차곡 채워 넣는다 . 
  6. 모든 객체의 이동이 끝나면 깨끗한 상태의 메모리를 얻는다 .

[ 세대별 가비지 컬렉션 ]

1 . 세대별 가비지 컬렉션의 동작

[ 가비지 컬렉션의 세대 ]

- 입구가 하나뿐인 버스에서 빨리 내리려는 승객은 출입구쪽에 , 늦게 내리려는 승객은 출입구 반대편에 위치한다 .

- CLR의 메모리도 구역을 나눠 메모리에서 빨리 해제될 객체 / 오래 살아남을 객체들을 따로 담아 관리한다 .

- CLR은 메모리를 [0,1,2] 3개의 세대로 나누고 0세대에는 빨리 사라질것으로 예상되는 객체를 , 2세대 에는 오래 살아남을 것으로 예상되는 객체들을 따로 담아 관리한다 .

- CLR은 객체의 나이가 어릴수록 메모리에서 빨리 사라진다 가정한다 .

- 나이는 가비지 컬렉션을 겪은 횟수를 말한다 .

- 1 세대에는 0 -2 로 넘어가는 과도기의 객체들이 위치한다 .


2 . 구체적인 동작

[ 가비지 컬렉션의 세대 ]

  1. 0세대가 할당된 객체들로 차오른다 .
  2. 할당된 객체들의 총 크기가 0세대 가비지 컬렉션 임계치에 도달하면 가비지 컬렉터는 0세대에 가비지 컬렉션을 수행한다 .
  3. 여기서 살아남은 객체는 1세대로 이동한다 .
  4. 새로 생성된 객체는 0 세대에 위치한다 . 다시 가득 차오르다 1세대의 컬렉션 임계치가 한계에 도달한다 .
  5.  1세대에 대해 가비지 컬렉션이 수행된다 . 가비지 컬렉터는 하위 세대에 대해서 가비지 컬렉션을 수행하기에 0,1 세대가 수행된다 .
  6. 2세대로 옮겨간 객체들은 더이상 다른 곳으로 옮겨가지 않는다 .
  7. 2세대가 포화되면 0,1 세대도 가비지 컬렉션이 이루어지기에 FullGC라고 불린다 .
  8. 힙의 각 세대는 낮을수록 가비지 컬렉션의 빈도가 높기에 살아남을 확률이 낮다 .
  9. 생명력이 강한 객체들이 계속 생성되면 2세대 힙이 가득 찰 것이다 .
  10. CLR은 애플리케이션의 실행을 잠시 멈추고 , FULL GC를 수행하여 여유 메모리를 확보하려 들 것이다 .
  11. 이때 , 애플리케이션이 차지하는 메모리의 크기가 크면 FULL GC의 시간도 길어지기에 정지 시간도 늘어날것이다 .

[ 효율적인 코드 작성을 위한 지침 몇가지 ]

1 . 객체를 너무 많이 할당하지 않는다

[ 객체를 너무 많이 할당하지 않는다 ]

- CLR의 객체 할당 속도가 빨라도,너무 많은 수의 객체 관리는 힙의 각 세대에 대해 메모리 포화를 초래하여 빈번한 가비지 컬렉션을 부른다

- 객체할당 코드를 작성시 필요한 객체인지 , 필요 이상으로 많은 객체를 생성하는 코드가 아닌지의 여부를 고려해야 한다 .


2 .  너무 큰 객체 할당은 피한다 

[ 대형 객체 힙]

- CLR은 보통 크기의 객체를 할당하는 힙과는 별도로 85KB 이상의 대형 객체를 할당하기 위한 대형 객체 힙을 따로 유지한다 

- 평소 사용하는 힙은 이와 대비하여 소형 객체 힙이라고 불린다 .

- 대형 객체를 소형 객체 힙에 할당하면 0 세대가 빠르게 차고 , 잦은 가비지 컬렉션을 야기하여 성능 저하를 일으킨다 .

- 이러한 이유로 CLR이 대형 객체 힙을 별도로 유지한다 .


[ 대형 객체 힙의 약점 - 할당시 ]

- 소형 객체 힙은 다음 객체를 할당할 메모리의 포인터가 위치한 메모리에 바로 객체를 할당한다 .

- 대형 객체 힙은 객체의 크기를 계산한 뒤 그만한 여유공간이 있는지 힙을 탐색하여 할당한다 .


[ 대형 객체 힙의 약점 - 가비지 컬렉션 수행후 ]

- 소형객체힙은 가비지 컬렉션을 수행후 해제된 메모리 공간에 인접 객체들을 끌어당겨 차곡 차곡 정리한다 .

- 대형객체 힙은 가비지 컬렉션을 수행후 해제된 공간을 그대로 둔다 . 이는 수MB에 이르는 메모리 복사비용이 너무 비싸기 떄문 .

- 메모리를 0바이트도 낭비없이 쓰는 소형 객체 힙과는 다르게 큰 공간을 군데 군데 낭비한다 .

- 대형 객체 힙은 할당 시의 성능뿐 아니라 메모리 공간 효율도 소형 객체 힙에 비해 크게 떨어진다 .


[ 대형 객체 힙의 약점 - 세대 ]

- CLR이 대형 객체 힙을 2세대 힙으로 간주하기에 대형 객체 힙에 있는 쓰레기 객체가 수거되려면 2세대에 대한

가비지 컬렉션이 수행되고 , 이는 FULL GC를 촉발하여 순간적인 애플리케이션의 정지를 불러온다 


3 . 너무 복잡한 참조 관계는 만들지 않는다 .

[ 참조 관계가 많은 객체의 가비지 컬렉션 ]

class A
{
	public C c;
}

class B
{
	public A a;
}

class C
{
	public A a;
    public B b;
}

class D
{
	public A a;
    public B b;
    public C c;
}

- 참조 관계가 많은 객체는 가비지 컬렉션 후에 살아남았을때가 문제이다 .

- 가비지 컬렉터는 가비지 컬렉션 이후 살아남은 객체의 세대를 옮기기 위해 메모리 복사를 수행한다 .

- 참조 관계가 복잡한 객체의 경우 단순히 메모리 복사에서 끝나지 않는다 .

- 객체를 구성하고 있는 각 필드 객체 간 참조 관계를 일일이 조사 , 참조하고 있는 메모리 주소를 전부 수정한다 .

- 클래스 구조가 간단했다면 메모리 복사만으로 끝날 일이 탐색과 수정까지 야기한다 .


[ 쓰기 장벽 ]

- D클래스를 예시로 들자면 , D 클래스는 2세대에서 살고 있을때 A형식의 필드 a를 새로 생성한 객체로 업데이트 됐다고 가정하자 .

- A 형식의 필드 a는 0세대에 있기에 루트가 없어 가비지 컬렉션의 위험에 처한다 .

- 이때 , CLR은 쓰기 장벽이라는 장치로 a 가 루트가 있다 가정하여 0세대 가비지 컬렉션을 모면하게 한다 .

- 다만, 쓰기장벽을 생성하는데 드는 오버헤드가 꽤 크다는 문제가 있다 .

- 참조 관계를 최소한으로 만들면 이런 오버헤드를 줄일 수 있다 .


4 . 루트를 너무 많이 만들지 않는다

[ 루트를 너무 많이 만들지 않는다 ]

- 가비지 컬렉터는 루트 목록을 돌며 쓰레기를 찾아낸다 .

- 루트 목록이 작아지면 그만큼 가비지 컬렉터가 검사를 수행하는 횟수가 줄어 성능에 유리하다 .


5 . 작은 구멍이 댐을 무너뜨린다 .

[ 습관 ]

- 잘못된 코딩 습관 몇 가지가 누적되다 보면 골치 아픈 문제가 되기에 , 공부한 내용을 염두한다면 지식들이 습관이 될 것이다 .