Garbage Collection(GC)

1. 개요

가비지 컬렉션이 무엇이고, 어떤 역할을 수행하는지 정리해봅니다


2. 메모리 생명 주기

giphy_lift_cycle

가비지 컬렉션에 대해 알아보기 전, 프로그래밍에서 데이터가 저장되는 메모리의 생명주기를 한번 짚고 넘어가야 합니다.

  1. 필요한 메모리 할당
  2. 할당된 메모리 사용(읽기, 쓰기)
  3. 해당 메모리가 필요 없어지면 해제

간략하게 말하면 메모리는 위와 같은 생명 주기를 가지고, 요약하면 생성, 사용, 해제의 세 단계를 가진다고 볼 수 있습니다.



3. 매니지드(Managed) 언어 vs 언매니지드(Unmanaged) 언어

그런데 여기서 1, 3번의 메모리 할당, 해제를 관할하는 대상이 누구냐에 따라 프로그래밍 언어는 매니지드(managed) 언어와 언매니지드(unmanaged) 언어로 나눌 수 있습니다. (엄밀히 따지면 메모리 영역 종류 중의 Heap영역의 관리 주체)

JavaScript에서의 메모리 모델

JavaScript의 표준인 ECMAScript의 명세에는 JS에서의 메모리 모델이 어떻게 구현되어야 하는지 기술되어 있지 않습니다. 브라우저별로 최적화를 위한 메모리 모델이 다를 수 있으므로, 여기서는 Heap영역이라고 지칭하지 않고, 포괄적으로 메모리 영역이라고 지칭하도록 하겠습니다 😅


3.1. 언매니지드 언어

그렇다면 언매니지드 언어의 메모리는 누가 관리하냐?

말 그대로 받아들여보자. ‘언’매니지드. 즉 언매니지드 언어라고 하면, 관리해주는 주체가 따로 없다는 뜻입니다.

그렇기 때문에 개발자가 관리의 주체가 되어 직접 메모리의 할당과 해제에 관여를 해야합니다.

해당 언어에서 개발자는 프로그래밍을 통해 메모리 할당부터 메모리 사용량 등을 직접 관리, 제어해야 하기 때문에 단편적으로 비숙련자에겐 러닝커브가 높을 수 있는 언어입니다.

대표적인 예로 C, C++이 있습니다.


3.2. 매니지드 언어

그렇다면 반대로 매니지드 언어는 메모리를 관리하는 주체가 누구이냐?

먼 길을 돌아왔습니다. 😂

바로 여기서 등장하는 것이 가비지 컬렉션(GC)의 개념입니다.

그리고 이 개념을 가지고 매니지드 언어에서 메모리를 관리해주는 기능이 바로 가비지 컬렉터라고 할 수 있겠습니다.


3.2.1. 그래서 가비지 컬렉션(Garbage Colleciton)이 뭔데?

가비지 컬렉션이란, 자동 메모리 관리 형식이라고 정의 내릴 수 있으며, 메모리 할당을 모니터링하고, 할당된 메모리 셀이 더 이상 필요하지 않은 시점을 확인하여 할당을 해제해주는 형식이라고 할 수 있습니다.


매니지드 언어의 대표적인 예로, 제가 다루고자 하는 JavaScript, C#, Java, Python등이 있습니다.


4. 가비지 컬렉션의 알고리즘, 메모리 관리 기법

4.1. 참조 (Reference)

가비지 컬렉션 알고리즘에서의 핵심 개념은 참조(Reference)입니다.

A라는 메모리를 통해 B라는 메모리에 접근할 수 있다면.

“B는 A에 참조된다”라고 합니다.


4.2. 가비지 컬렉션 알고리즘

브라우저별로 혹은 JavaScript 엔진 별로 가비지 컬렉션을 수행하는 알고리즘은 다르겠지만, 일반적으로 자주 쓰이는 알고리즘으로 소개되는 두 알고리즘을 정리합니다.

소개할 알고리즘은 어떤 객체를 “더 이상 필요없는 객체“로 판단하느냐가 다른데, 이 내용에 초점을 맞추어 이해하면 이해하기 더 쉽습니다.


4.2.1. 참조-세기(Reference-counting) 알고리즘

참조-세기 알고리즘은 번역이 더 어색한 경우에 속합니다. 영어 자체로 reference-counting. 즉, 객체에 대한 참조 개수를 센다(counting)으로 이해하면 좋을 것 같습니다.

참조-세기 알고리즘은 참조 개수를 “센다”라고 했습니다. 다시말해 이 알고리즘은 ‘더 이상 필요없는 객체’를 ‘어떤 다른 객체도 참조하지 않는 객체‘라고 정의합니다.

특정 객체를 참조하는 객체가 하나도 없다면, 그 객체에 대해 가비지 컬렉션을 수행하게 됩니다.


아무 객체에게도 참조되지 않는다는 것은

애초에 메모리에 직접 접근하는 것 외엔 접근할 방법이 없다는 뜻이므로, 상식적으로도 “더 이상 필요없는 객체”입니다.


다만, 이 알고리즘에는 치명적인 문제가 있습니다.

이 문제는 두 객체가 서로를 참조하면 발생하게 되는데,

예를 들어, 만약 어느 함수에서 서로 참조를 하는 두 객체가 있다고 가정했을 때,

함수가 종료되면 메모리가 해제되어 메모리 풀(memory pool)에 회수되어야 하지만, 함수가 종료되어도 참조되는 개수가 0이 아니므로 메모리 풀에 회수가 되지 않는 문제가 발생합니다.

이를 순환 참조라고 합니다.

1
2
3
4
5
6
7
8
9
function func() {
var x = {};
var y = {};
// 순환 참조
x.a = y;
y.a = x;

return;
}

출처 : 자바스크립트의 메모리관리 - MDN


4.2.2. Mark-and-sweep 알고리즘

참조-세기 알고리즘의 한계를 해결할 수 있는 알고리즘이면서 ‘더 이상 필요없는 객체’를 ‘닿을 수 없는 객체‘로 정의하는 알고리즘입니다.

이 알고리즘은 roots라는 객체의 집합을 가집니다.(js에서는 전역 변수를 의미)

참조되는 객체간의 참조관계를 roots로부터 그리며,

가비지 컬렉터는 주기적으로 roots로부터 시작해서,

roots가 참조하는 객체들, roots가 참조하는 객체가 참조하는 객체들을 쭉쭉 이어서 각 객체를 접근할 수 있는 객체라고 표시(Mark)합니다.


그 후, roots로 부터 참조관계를 따라가 접근할 수 없는 객체가 있다면, 해당 객체를 더이상 필요없는 객체라고 판단하고, 가비지 컬렉션을 수행하는 알고리즘입니다.

함수 내의 순환 참조하던 객체들도 roots로 부터 ‘닿을 수 없는 객체’이므로 가비지 컬렉션이 수행될 수 있게 됩니다.


동작 방식

  1. Mark

    • 객체가 생성될 때마다 mark bit가 0(false)로 설정

    • mark 단계에서 모든 접근 가능한 객체의 mark bit가 1(true)로 설정

  2. Sweep

    • mark 단계 이후 mark bit가 여전히 0(false)이면 ‘닿을 수 없는 객체’이므로 가비지 콜렉터가 수집해 메모리에서 해제됨

4.3 더 읽어보기


5. 결론

메모리 관리 어려워서 러닝커브 높다매? 그러면 가비지 컬렉터있는 매니지드 언어가 메모리 관리해주면 편하고 좋은 거 아님?

  • 꼭 그렇지만은 않다. 자동으로 메모리를 관리해준다는 것은 메모리가 언제 해제될지 예측할 수 없다는 것으로, 수동으로 메모리를 해제할 지 결정하는 것이 편리할 때가 있다.
  • 또, 정말 숙련된 개발자라면, 알고리즘에서 벗어나 메모리 누수(memory leak)을 일으키는 메모리에 대한 관리를 더 잘해줄 수도 있는 것이다.

참고