Study

가비지 컬렉터(Garbage Collector, GC)

mins_s 2024. 7. 10. 19:05

오늘은 가비지 컬렉터(Garbage Collector, GC)에 관한 게시글을 작성하려한다.

 

우리가 프로그래밍을 할 때, 때에 따라 동적으로 메모리를 할당해야하는 상황이 주어진다.

이때 할당된 메모리는 힙 영역에 저장이 되고, 주소값을 통해 힙 영역에 저장된 데이터에 접근하게 된다.

 

하지만 이러한 메모리 공간은 유한하기 때문에, 메모리 공간의 관리를 잘 해주어야 한다.

 

C, C++과 같은 언어에서는 프로그래머가 동적으로 메모리를 할당하였다면(malloc, new) 수동으로 그 메모리를 해제(free, delete)해주어야 하는 빈번한 과정을 거쳐야 했다.

 

꼼꼼한 검수과정을 통해 수동으로 메모리를 해제해주는 작업을 잘 거친다면 메모리 누수 현상은 발생하지 않을 것이다.

하지만 그런 꼼꼼한 검수 과정을 거치더라도 메모리 누수 현상을 완벽하게 방지할 수는 없다...(사람은 누구나 실수를 하기 때문)

 

이러한 문제를 해결하기 위해 가비지 컬렉터가 나오게 된 것 이다.


가비지 컬렉터 작동 방식

가비지 콜렉터의 작동 방식은 크게 2가지 방식이 있다.

1. 추적 기반 가비지 컬렉터(Tracing Garbage Collection)

프로그램을 실행하는 도중 특정한 시점이 되었을 때 현재 할당된 모든 메모리를 조사하는 방식이다.

이때 할당된 메모리들은 현재 접근 가능한지 불가능한지 분류한 뒤, 접근이 불가능할 경우 가비지로 간주하여 해제시키는 방식이다.

여기서 접근 가능한 메모리를 root라고 한다. 

 

Mark-and-Sweep

Mark-and-Sweep은 위에 있는 설명을 그대로 구현한 방식이다.

GC를 실행하여 접근이 가능한 메모리를 대상으로 마킹(Marking)을 한 이후, 마킹이 되지 않은 메모리를 할당 해제(Sweep) 하는 방식이다.

위와 같이 힙 영역에 할당된 메모리 obj1, obj2, obj3, obj4가 있다고 가정하자.

GC가 실행되어 할당된 메모리 중 접근이 불가능한 메모리를 마킹한다.

여기선 obj1, obj3가 접근이 불가능한 메모리로 가정한다.

마킹된 메모리는 더 이상 필요하지 않는 메모리이므로 할당을 해제해준다.

 

하지만 지속적으로 메모리 할당과 해제를 반복하게 될 경우 중간 중간 빈 공간이 생기게 되는 메모리 단편화가 생기게 되어,연속적으로 메모리를 할당해야할 경우 남은 메모리가 충분함에도 불구하고 할당하지 못하는 상황이 발생한다.

 

Compact

이러한 문제점을 보완하기 위해 Mark, Sweep 과정을 거친 이후 Compact 과정을 한번 더 거치는 방법을 채택하였다.

 

Compact 과정은 간단하다.불필요한 메모리를 해제한 이후 남은 메모리들을 앞에서 부터 차례차례 빈공간을 채워나가는 과정이다.

 

추적 기반의 방식은 중간에 메모리가 변경되면 마킹을 다시해야하는 상황이 발생하므로 프로그램 전체를 정지(Stop-The-World) 시켜야 한다.

그렇기 때문에 중간 중간 끊기는 현상이 발생하여 사용자의 경험에 좋지않다는 문제가 발생한다.

 

1.1 점진적 가비지 컬렉터(Incremental Garbage Collection)

이 방식은 기존 방식에서 중간 중간 끊기는 현상이 발생하는 부분에 대해 어느정도 보완을 하기위해 만들어졌다.

기존 한 번에 마킹과 해제를 하는 방식과는 다르게 여러 번에 걸쳐서 수행하는 방식이다.

 

여러 번 걸쳐서 수행하는 만큼 마킹과 해제를 하는 한 사이클이 오래 걸리지만 프로그램이 정지하는 시간은 줄일 수 있다.

 

삼색 표시 기법(Tri-Color Marking)

점진적 가비지 컬렉터는 메모리를 세 가지로 마킹하는 방식인 삼색 표시 기법을 사용한다.기본적으로 흰색, 회색, 검은색으로 표현되며 각 색깔 별 의미는 다음과 같다.

  • 흰색 - 더 이상 접근 불가능한 객체
  • 회색 - 접근이 가능한 객체이지만, 이 객체에서 가리키는 객체들이 아직 검사되지 않은 객체
  • 검은색 - 접근이 가능한 객체

그리고 이 기법은 다음과 같은 순서로 이루어진다.

먼저 모든 객체를 흰색으로 표시한다.

그 이후 루트 집합(전역 변수, 스택 변수 등)에 도달 가능한 객체를 찾아 회색으로 표시해준다.

회색으로 표시된 객체가 참조하는 모든 객체를 회색으로 표시하고 자기 자신을 검은색으로 표시한다.

모든 회색 객체가 없어질 때까지 위와 같은 과정(1~3번)을 반복한다.

그 이후 남은 흰색 객체는 접근 불가능한 객체이므로 모두 해제한다.

(이 방식은 BFS(Breadth-First Search) 알고리즘과 유사하다.)

 

1.2 세대별 가비지 컬렉터(Generational Garbage Collection)

마찬가지로 이 방식도 기존 방식에 문제점을 보완하기 위해 만들어졌다.

 

위의 방식들을 토대로 지켜보았을 때 대부분의 객체들은 만들어진 이후 잠깐 쓰이고 금세 버려지며, 오래 살아남아서 쓰이는 경우는 그렇게 많지 않다는 경향을 발견하여 이 방식을 만들게 되었다.

 

Generation

위 경향을 토대로 크게 두 가지 영역(세대)으로 나누어 수집 대상을 정했다.

  • Young 혹은 New - 새로 할당된 객체 혹은 일정 수준 이하 동안 살아남은 객체들
  • Old - 새로 할당된 이후 일정 수준 이상 동안 살아남은 객체들

여기서 살아남았다는 말은 가비지 컬렉터가 호출된 이후 수집이 안된 횟수를 뜻한다.

즉, 일정 호출 횟수 이후까지 살아남아 있다면 그 객체는 Old세대가 되는 것이다.

 

또한 Young세대는 Old세대보다 비교적 더 작은 크기를 가지게 되는데,

그 이유는 전체의 일부분을 지속적으로 빠른 시간내에 GC가 수집하면서 메모리 공간을 확보하기 위해서이다.

 

Minor-GC와 Major-GC(Full-GC)

세대별로 나누어 수집하는 방식은 크게 두가지 방식으로 나뉜다.

 

1. Minor-GC

특정 조건에 만족하여 Minor-GC가 호출될 땐 Young세대에 있는 객체들을 대상으로 수집이 이루어진다.

빨간 색으로 표시된 객체들은 GC가 호출이 되었을 때 수집되는 대상이다.

Minor-GC로 호출되었기 때문에 Young세대에 있는 수집 대상만 마킹되고 수집하는 것을 확인할 수 있다.이때 수집된 곳은 빈 공간(Empty)이 된다.

 

2. Major-GC

특정 조건에 만족하여 Major-GC가 호출될 땐 전체 객체를 대상으로 수집이 이루어진다.

위의 Young세대 수집이 있은 이후의 객체들은 특정 횟수 이상을 만족하며 Old세대로 옮겨지고,

Young세대에는 새로운 객체들이 할당된 상태이다.

 

Major-GC로 호출되었기 때문에 전 세대에 있는 수집 대상들을 마킹하고 수집하는 것을 확인할 수 있다.

전체 대상으로 수집하기 때문에 Full-GC라고 부르기도 한다.


2.  참조 횟수 카운팅 기반 가비지 컬렉터(Reference Counting based Garbage Collection)

다른 메모리가 어떤 메모리를 얼마나 많이 참조하는지 횟수를 세어서 접근 가능과 불가능을 나누는 방식이다.

 

Reference Counting

한 메모리가 다른 메모리를 참조하면 그 다른 메모리는 참조 횟수에 1을 더하고 참조를 중단하면 참조 횟수에 1을 뺀다.

이때 1을 뺀 이후 참조 횟수가 0이 되면 해당 메모리에 접근이 불가능하므로 그 메모리를 해제하는 것이다.

 

단지 참조 횟수를 카운팅 해주고 0이 되었을 때 해제해주기 때문에 구현이 간단하다는 장점이 있다.

 

OverHead

하지만 매번 대입를 할 때마다 참조 횟수를 카운팅 해주고 참조 횟수가 감소될 때 조건문을 통해 해제가 가능한지 판별하는 것은 적지 않은 오버헤드를 발생시킬 수 있다 또한 참조 횟수를 저장해야하기 때문에 캐시효율 또한 낮아질 수도 있다.

 

Cyclic Reference

이러한 문제점 말고 한 가지 더 문제점이 있는데 그것은 바로 순환 참조이다.

 

예를 들어 A가 B를 가리키고 B에서 A를 가리키면 둘 다 참조 횟수가 1이 될 것 이다.

이때 A와 B 둘 중 어느 곳에도 접근할 수 없는데 참조 횟수가 0이 아니기 때문에 메모리를 해제할 수 없게 되며 메모리 누수가 발생하게 된다는 문제점이 있다.


현재는 위의 두 가지 방식중 대부분의 언어는 Tracing GC를 채택하고 있다.


마무리

가비지 컬렉터가 동적으로 할당하는 메모리를 어떤 식으로 관리하는지에 대해 알아보았다.

 

기존 수동으로 관리하는 방식에 비해 매우 편해졌지만 아직까지 가비지 컬렉터에게 모든 관리를 도 맡기에는 한계가 있다.

 

가비지 컬렉터가 수집하는 조건은 더 이상 참조하지 않는 객체에 대해만 할당을 해제하기 때문에 메모리 누수의 가능성은 항상 열려있다.또한 실행 시간에 작업을 수행하는 이상 성능 저하는 피할 수는 없다.

 

그렇기 때문에 가비지 컬렉터를 너무 맹신하고 메모리에 대한 문제를 신경쓰지 않는다면 심각한 성능 저하를 일으킬 수 있는 것이다.