Deep Dive/Javascript

JavaScript's GC (Garbage collector)

dev4us 2021. 6. 29. 23:23

우리는 언어를 이용하여 다양한 어플리케이션을 개발하며 필요한 수 많은 변수, 함수 같은 데이터를 선언합니다.

필요에 의해 데이터를 할당하고 사용하는 과정은 개발자의 몫이지만, 더 이상 필요로 하지 않는 데이터는 어떻게 될까요?

제일 좋은 방안은 더 이상 필요가 없는 데이터를 정리하여 쾌적한 환경을 유지하는 것일겁니다.

그러면 이러한 메모리를 수거해주는 역할은 어디서 수행되며 어떤 것이 불필요한 메모리라고 판단할 수 있는걸까요?

이번 포스트에서는 위 역할을 수행해주는 Garbage Collector에 대해 알아보겠습니다.

가비지 컬렉터(Garbage Collector)란 위에서 언급한 듯 메모리 할당을 추적하고 할당된 메모리가 더 이상 필요 없어졌을 때 이에 할당된 메모리를 회수하는 작업입니다. (이하 GC)

이 작업은 Javascript 내에서 자동적으로 실행되어 메모리 관리를 하는 특징을 가지고 있습니다.

그러면 한가지 의문이 생깁니다.

특정 메모리가 사용 중인지 또는 이후에 사용될 지 GC는 어떻게 알고 정리할 수 있는 걸까요?

혹시나 실수하여 사용하고 있는 데이터를 소거하는 경우는 없을까요?

아래 GC가 이루어지는 자세한 과정을 함께 보며 궁금증을 해소해보도록 하겠습니다.

# 메모리 구조

먼저 선언이 이루어진 뒤 해당 데이터는 메모리의 빈 공간에 할당될 것입니다.

이 할당되는 공간을 힙 메모리(Heap Memory)라고 합니다.

힙 메모리는 위와 같이 구성되어 있습니다.

(실제로는 Large Object Space, Code Space 등 공간이 더 존재하지만 GC가 발생하는 영역만을 그렸습니다.)

이 중 New Space(Young Generation)는 새로 만들어진 데이터가 우선적으로 저장되는 공간입니다.

2개의 Semi Space로 이루어져 있으며 'to'와 'from'의 태그를 통해 분류가 가능합니다.

Old Space(Old generation)는 New Space에서의 1차적인 GC(Major GC)가 이루어진 이후에 데이터가 저장되는 공간으로서 'Old Pointer Space'는 다른 객체를 참조하는 객체가 저장되는 공간이며 'Old Data Space'는 문자열과 같은 다른 객체를 참조하지 않는 데이터들이 저장되는 공간입니다.

# New Space 에서의 Minor GC

데이터 할당이 시작하면 두 종류의 Semi-space 중 to 영역에 메모리 할당이 이루어집니다.

이후 더 이상 To-space에 메모리 할당이 불가능할 경우 Minor GC가 실행됩니다.

Minor GC가 시작되면 To-space에 생성된 객체들은 모두 From-space로 이동됩니다.

이후 객체 그래프를 순회하며 어플리케이션에서 사용 중인 메모리를 찾아내어 다시 To-space로 이동시킵니다.

이 과정에서 To-space에서는 자동으로 압축을 진행합니다.

(A, B, E 사이에 빈 공간이 존재하지 않음)

이는 조각화 현상을 막아 데이터 적재에 용이하게 보관하기 위함입니다.

이후에 From-space에 남아 있는 객체들은 모두 Garbage로 판단하고 삭제합니다.

이와 같은 방식을 Scavenge 알고리즘을 통한 Minor GC라고 합니다.

# Old Space 에서의 Major GC

위와 같은 Minor GC가 이루어진 뒤 다시 To-space에 더 이상 메모리를 할당할 수 없는 상황이 발생하면 같은 Minor GC가 실행됩니다.

하지만 두 번의 Minor GC를 거친 뒤 소거되지 않은 메모리는 별도의 공간으로 이동되어 관리됩니다.

이 별도의 공간은 Old Space로서 Major GC라고 불리는 다른 방식을 통하여 관리됩니다.

Major GC는 마찬가지로 Old Space 영역이 가득 찼을 때 발생하며 Mark, Sweep, Compact 단계로 이루어져 있습니다.

1. Marking Phase

먼저 Marking Phase 에서 객체 별 참조 여부를 확인하여 'White, Gray, Black'으로 마킹을 진행하는데 White는 GC가 아직 탐색을 진행하지 못한 객체를 의미하며 Gray는 GC가 탐색은 마쳤으나 참조하고 있는 객체의 유무를 확인하지 못한 상태, Black은 참조하고 있는 객체가 존재함을 확인한 상태를 나타냅니다.

모든 객체는 'White' 상태로 시작되며 먼저 Root 객체를 Gray로 마킹합니다. 이후 인접 객체를 Gray로 마킹한 뒤 실제로 참조 관계가 있을 경우 Black으로 마킹합니다.

2. Sweep Phase

Sweep Phase에서는 위 Marking Phase를 거치고 난 뒤 White로 마킹되어 있는 객체를 모두 제거합니다.

(실제로는 Free Space로 이동시키며 Free List에 해당 메모리를 추가합니다.)

3. Compact Phase

Compact 단계에서는 Minor GC 때와 마찬가지로 조각화 현상을 방지하고 추가적인 메모리를 확보하기 위해 진행됩니다.

# 왜 Minor GC, Major GC 두 가지 방식으로 GC가 실행될까요?

물론 한 가지 일관된 방식으로 메모리가 관리된다면 좋겠지만 위 Minor GC는 속도가 빠르다는 장점을 가지고 있는 방면에 GC 과정 중 물리적인 데이터 백업을 필요하므로 메모리 공간에 대한 오버헤드가 존재합니다.

그래서 보다 큰 메모리 영역이 필요한 Old Space에서는 Mark-Sweep-Compact 알고리즘(DFS)을 이용한 방식인 Major GC를 필요로 하는 것입니다.

# 글을 마치며

이로써 Javascript에서의 Garbage collector 동작 방식에 대한 설명이 끝났습니다.

위 내용을 통하여 알 수 있는 점은 참조라는 개념이 굉장히 중요하다는 것을 알 수 있습니다.

더더욱 Javascript의 경우 'prototype와 같은 절대적 참조'나 '속성값과 같은 지정적 참조'가 존재하기 때문에 일반적인 객체보다 더 확장된 범위로 고려하여 생각하는 방식이 필요할 것 같습니다.

또한 Javascript 내에서 GC는 자동적으로 실행되어 메모리 관리를 진행해주지만 개발자가 메모리 관리에 대한 부분을 전혀 신경 쓰지 않는다면 메모리(memory leak)이 발생하여 어플리케이션의 성능 저하나 심각한 오류를 초래할 수 있다는 점을 꼭 염두해야 합니다.

결국 GC를 통하여 메모리를 회수한다는 것은 비결정적인 동작이기 때문입니다.

끝.