C#

[C#] GC 가비지 컬렉션

자가라o 2021. 10. 17. 23:02

Garbage Collection 통칭 GC는 메모리 관리기법의 하나.

CLR에서 GC는 자동메모리 관리자 역할어플리케이션의 메모리 할당 및 해제를 관리합니다.

 

CLR : MS .NET의 가상머신 구성요소로 IL 실행을 담당합니다.

 


 

<기본적인 동작 방식>

 

1) 현재 수행중인 스레드를 모두 중단하고 GC스레드를 활성화합니다.

2) 사용중인 객체 참조그래프 : 루트를 생성합니다.

 

루트 참조

  • 각 스레드가 수행중인 메서드 로컬변수
  • CPU 레지스터 변수가 가지고있는 참조
  • 사용중인 각 타입(클래스)의 정적필드
  • 전역변수

 

 

3) 힙 메모리의 모든데이터는 처음에 쓰레기로 간주합니다.

4) GC는 루트를 참조하여 관계있는 데이터를 쓰레기에서 제외합니다.

 

메모리 컴팩션

 

5) 쓰레기 데이터를 삭제하고 현재 사용중인 객체의 위치를 재조정(메모리 컴팩션)합니다.

 

메모리 컴팩션(Memory Compaction)

찾아보면 '동적으로 시스템을 구성하는 기억장소에서 여러곳에 흩어져 있는 가용 공간을 하나의 연속된 공간으로 만드는 작업. 기억장소의 할당과 반납과정에서 일어난 단편화 현상을 제거한다.' 고 하는데

대충 쉽게 띄엄띄엄 사용중인 메모리들을 한곳으로 잘모아서 빈공간을 늘리는 일종의 조각모음이라고 할 수 있습니다.

 


 

GC가 효율적으로 작동하기 위해 다양한 방식이 제시되 었는데 추적기반의 점진적GC, 세대별GC와 참조 카운팅GC등의 방법이 있습니다.

가장 흔하게 쓰이는 세대별 GC방식만 알아보겠습니다.

 

<세대별 GC>

 

메모리를 구역별로 나누어 빨리 해제될 객체오래 남아있을 객체를 따로 관리합니다.

메모리를 0, 1, 2세대로 구분하여 0세대는 방금 생성된 객체나 빨리 사라질것으로 예상되는 객체, 2세대는 오래남을 것으로 예상되는 객체로 나뉩니다. 수명 예측 방법은 GC를 겪은 횟수로, 횟수가 딱 정해진 것이 아니라 프로그램에 따라 다르게 측정됩니다.

 

▶ 작동방식

  • 애플리케이션 실행 후 힙에 할당하다가 (할당 즉시 0세대) 할당된 총 크기가 0세대 GC의 임계치에 도달하면 0세대 가비지 컬렉션을 수행합니다.
  • 여기서 살아남아 일정 횟수의 GC를 겪은 객체는 1세대로 이동합니다.
  • 1세대 임계치 도달시 1세대 GC, 2세대 임계치 도달시 2세대 GC로 반복합니다.

세대별 GC 실행시에는 이전 세대 또한 포함해서 진행합니다.

0세대 GC 실행시 0세대만 실행하지만

1세대 GC 실행시에는 1, 0세대,

2세대 GC 실행시 2, 1, 0세대 모두 GC를 실행하며 이를 Full GC라고 합니다.

 

GC 실행시 애플리케이션의 실행을 잠시 멈추고 GC를 실행한다고 했는데

애플리케이션 메모리가 커지면 -> Full GC의 오버헤드도 커지게 되고 -> 정지시간 또한 늘어나게 됩니다.

그래서 실시간 프로그램에서는 GC가 효율적으로 동작할 수 있도록 신경쓰는게 중요합니다.

 


 

<객체 크기별 GC 작동방식>

 

C#의 CLR에서는 객체의 크기에 따라 메모리를 크게 2가지로 나눠서 관리합니다.

 

1) SOH (Small Object Heap)

SOH는 용량이 작은 객체 (85KB 미만)

 - 세대별 GC방식 그대로 사용됩니다.

 

2) LOH (Large Object Heap)

LOH는 용량이 큰 객체 (85KB 이상)

 - [C/C++ 방식과 유사] 할당, 해제는 GC에서 하지만 메모리 컴팩션이 일어나지는 않습니다. (해제되면 된대로 냅둠)

용량이 큰경우 오히려 메모리를 재배치하는 오버헤드가 크기 때문에 2세대 GC에서만 메모리를 해제합니다. (하지만 이때도 많은 오버헤드) 그렇기에 2세대 GC가 자주 일어나지 않게하고 큰용량의 생성,해제도 반복해서는 안됩니다.

 


 

<GC 장점>

  • 유효하지 않은(이미 해제된) 포인터 접근을 방지합니다.
  • 이중해제를 방지합니다.
  • 메모리 누수를 방지 할 수 있습니다.

 

<GC 단점>

  • 어떤 메모리를 해제할지 결정하는데도 비용이 듭니다.
  • GC가 일어나는 타이밍이나 점유시간을 예측하기 어렵습니다.
  • 메모리 해제 시점을 알 수 없습니다.

 

<GC 사용지침>

  • 지나치게 많은 할당 X
  • 너무 큰 객체 할당 X
  • 너무 복잡한 참조 X
  • 너무 많은 루트 X

 


 

<GC 메서드>

 

GC.Collect() 모든 세대 GC 즉시 수행
GC.Collect(int) 0세대에서 지정된 세대까지 GC 즉시 수행
GC.CollectionCount(int) 지정된 세대의 개체에 대해 GC가 수행된 횟수 반환
(GC가 언제 발생하는지 모니터링하는 가장 쉬운방법)
GC.GetGeneration(object) obj의 현재세대 반환
GC.MaxGeneration 시스템에서 현재 지원하는 가장 큰 세대 번호를 가져옵니다.
(설명은 0,1,2세대라고 했지만 시스템에 따라 0~n세대로 나뉠수도 있기때문)

 


 

[추가] C / C++ 방식

 

new, malloc으로 힙에 동적 메모리 할당 / delete, free로 동적 메모리 해제

자유 메모리 블록 리스트 (Free memory block list)로 관리 된다고 합니다.

 

할당 : 메모리 앞쪽부터 필요한만큼의 메모리 할당이 가능한 공간을 찾아 할당 후 초기화합니다.

해제 : 해제 후 주변의 빈 메모리와 합쳐집니다.

 

단점 : 파편화 - 전체 빈 메모리는 많지만 실질적 할당 실패 가능성이 커집니다.

Heap의 앞에서 부터 메모리를 탐색하므로 빈메모리가 뒤쪽에 많다면 매번 오버헤드가 발생할 수 있습니다.