자바스크립트 GC(Garbage Collection)에 대하여
다니는 회사에서 한 달 주기로 기술 공유를 하는 시간이 있는데, 얼마전에 JS의 GC 발표를 했었다. 사실 JS의 GC는 이미 잘 정리된 블로그들이 많아서 블로그에 올리지 않으려 했는데, 일요일에 스터디 카페와서 목적을 이뤘는데 폭우로 발이 묶여 뭐하지? 생각하다가 작성해본다.
Garbage Collection?
간단히 말해서 사용되지 않는 메모리를 관리하는 기술이다.
C 언어는 malloc, free
같은 함수로 직접 메모리를 할당하고 해제하지만, 대부분의 고 수준 언어들은 Garbage Collection(GC) 이라 불리는 기법을 이용해 자동으로 메모리를 관리한다.
자바스크립트가 실행될 때 메모리
JS 코드가 실행되면 stack과 heap 메모리 영역을 사용하는데, 이 때 GC는 heap 메모리의 New space
와 Old space
에서 동작한다. 간단하게 heap 메모리 영역을 정리하면 아래와 같다.
New space
- 객체가 새로 생성되면 할당되는 곳으로 대부분 수명이 짧다.
- Scavenger라고 불리는
Minor GC
에 의해 관리된다.
Old space
- Minor GC가 두 번 동작하는 동안 New space에서 살아남으면 Old space로 이동하게 된다.
Major GC(Mark-Sweep & Mark-Compact)
에 의해 관리된다.
Large Object space: 다른 공간보다 크기가 큰 객체들이 할당되는 공간
Code space: JIT 컴파일러가 컴파일 된 코드 블럭을 저장하는 곳.
Minor GC (Scavenger)
새로 추가되는 객체는 Heap 메모리의 New space
공간에 할당되는데, 새 객체를 할당될 때마다 증가되는 포인터가 있다. 이 포인터가 객체가 추가됨에 따라 New Space 메모리의 끝을 가리키고, 이 상태에서 객체가 추가되면 Minor GC가 동작한다.
Minor GC가 어떻게 동작하는지 그림으로 살펴보자. 아래 그림을 보면 New Space
메모리 공간이 2개의 영역으로 구분된다. 현재 From 쪽에 객체들이 할당되어 있고 포인터는 New Space
의 끝을 가리키고 있다. 이 때 새로운 객체 7번이 할당되는 사건으로 Minor GC가 발생한다.
Minor GC는 우선 사용중인 객체 1,3,5를 다른 공간으로 옮기고 사용하지 않는 객체인
2,4, 6은 제거한다.
그리고 7번 객체를 1,3,5가 있는 공간에 할당한다.
이 때 다시 객체가 할당되어 포인터가 New Space
의 끝을 가리킨 상태에서 10번 객체가 들어오면 Minor GC가 발생한다.
이 때 Minor GC 과정에서 2번 동안 살아남은 객체 1,5는 Old Space로 이동되어 Major GC에 의해 관리된다. 한 번 살아남은 객체 7,9는 New Space
내부의 다른 영역으로 이동되고 사용되지 않은 객체 3, 8은 제거된다.
Minor GC 과정은 이게 전부다. 정리해보면
- 새로운 객체는 Heap 메모리 영역의 New Space 공간에 할당된다.
- 할당 포인터가 끝에 도달하면 Minor GC가 수행되는데, 2번 동작하는 동안 살아남은 객체는 Old Space로 이동한다.
- 할당된 메모리가 상대적으로 작아 자주 발생하고, 빠르게 동작하도록 설계되었다.
왜 Minor와 Major GC가 있을까?
JS 코드가 실행되며 생성되는 객체들이 대부분 생성되고 사용 후 바로 제거되는 특성에 기인한다. 전체를 하나의 공간에 두고 관리하는 것보다 Minor GC에 의해 살아남은 객체들을 관리하는게 효율적이기 때문이다.
Major GC
메이저 GC는 Mark-Sweep-Compact
알고리즘을 사용하며 각 특성은 아래와 같다.
mark: GC 루트부터 힙 구조를 DFS 방식으로 순회하여 사용중인 객체는 활성상태로 표시한다. 구체적으로는 비트의 활성상태를 true로 표시한다.
sweep: 힙 메모리를 스캔하여 비트의 활성상태가 false 라면 메모리를 해제한다.
compact: 활성상태의 객체를 메모리의 동일한 페이지로 이동시킨다.
Orinoco
GC의 중요한 지표 중 하나는, 메인 스레드가 GC를 수행하기 위해 중지되는 시간이 얼마나 짧느냐이다. Orinoco
는 메인 스레드를 최대한 GC로부터 해방시켜 주기 위해 병렬, 증분, 동시성을 반영한 더 좋은 GC를 만들기 위한 프로젝트 코드네임
이다. 아래에서는 좋은 GC를 만들기 위해 사용된 병렬, 증분, 동시성에 대해 정리한다.
병렬 (Parallel)
메인, 헬퍼 스레드가 동일한 시간동안 GC를 수행하는 것이다.
Stop-the-world 접근 방식이지만 중단되는 시간이 Thread 개수만큼 나뉘고 Heap 메모리가 변하지 않기 때문에 (메인 스레드가 GC를 수행하기 때문에) 가장 쉬운 방법이다.
증분 (Incremental)
메인 스레드가 간헐적으로 소량의 GC 작업을 수행하는 방법이다. 상대적으로 전체 GC를 수행하는 것보다 짧은 지연시간 여러개로 분산 시킬 수 있고, 메인 스레드 실행 시간에 거의 영향을 주지 않는다.
동시성 (Concurrent)
메인 스레드는 자바스크립트를 실행하고, 헬퍼 스레드가 뒤에서 GC 작업을 수행하는 방법이다. heap 메모리가 실시간으로 변하기 때문에 세 가지 방법 중에는 가장 어려운 기술이다.
V8 엔진과 Major GC
위에서 알아본 병렬, 증분, 동시성이 더 좋은 GC를 만들기 위한 기술이었다면 V8 엔진은 Major GC를 어떻게 수행할까?
우선 메인 스레드가 JS를 실행하는 동안 백그라운드에서 헬퍼 스레드가 동시 마킹을 진행한다. 동시 마킹이 완료되거나, 메모리 제한에 도달하면 메인 스레드가 남은 마킹들을 완료하고 헬퍼 스레드와 같이 압축 및 포인터 업데이트를 수행한다. 이때 약간의 일시중지 시간이 발생하지만 대부분 10ms 이하로 진행되어 사용자들의 작업이 방해받지 않도록 한다.