[JS] 자바스크립트와 가비지 컬렉팅
코드가 실행되면 컨택스트가 만들어지고 콜스택에 쌓입니다. 이 과정에서 환경정보를 수집해 데이터를 메모리에 할당하고 식별자에 연결합니다. 그렇다면 코드가 전부 실행되서 콜스택에 컨텍스트가 사라지면 메모리에 있는 데이터들은 어떻게 될까요?
일단, 당연히 메모리에서 사라져야 합니다. 그렇지 않으면 작업을 할 때 마다 쌓이기만 해서 메모리 용량을 초과해버릴테니까요. 컴퓨터가 뻗어 버리기 전에 메모리 제거를 해줘야 하는데, 우리는 자바스크립트로 프로그래밍을 할 때 이 작업을 해본 적이 없습니다. v8의 가비지 컬렉터가 대신 해주고 있기 때문입니다.
V8 메모리 구조
가비지 컬렉터의 작업 방식을 이해하기 위해선 먼저 자바스크립트 엔진 v8의 메모리 구조를 알아야 합니다.
Resident Set
V8엔진의 메모리 구조는 Resident Set이라고 부르는 형태로 프로그램 실행 시 메모리 영역에 추가됩니다. Resident Set은 스택과 힙영역으로 나뉩니다. 자바스크립트가 싱글 스레드 기반이기에 하나의 스택으로 작업처리를 하고, 컴파일 시 크기를 알 수 없는 데이터는 런타임 중에 힙영역에 담아 스택에서 참조하도록 합니다.
힙 영역
콜스택의 데이터는 함수호출이 끝난 뒤 OS에 의해 정리되지만, 힙 메모리는 그렇지 않습니다. 힙영역에는 참조형 데이터의 실제 값이 담겨있기 떄문입니다. 함수 호출이 끝났더라도 해당 영역이 참조된 곳이 존재 한다면 값은 사라지지 않습니다.
따라서, 가비지 컬렉터로 참조여부를 검사해 힙영역을 제거를 해줘야 합니다.
가비지 컬렉터
힙 영역엔 New space, Old space, Large Object space,코드 space, 셀 space, 속성 space, 맵 space가 있는데 이중에서도 New space, Old space에서 가비지 컬렉션이 발생합니다. The Generational Hypothesis
가설은 대부분의 경우 새로운 객체가 오래된 객체보다 버려질 가능성이 높다고 합니다.
그래서 자바스크립트의 GC는 만들어진 기간과 상관없이 모든 객체를 매번 검사하기 보다 영역을 두개로 나눠 효율적으로 진행합니다. 새로운 객체는 작은 용량으로 만들어진 공간에서 자주 GC를 진행하고 오래된 객체는 좀 더 큰 공간에서 GC를 진행해 메모리 낭비를 최소화하는 방식입니다.
New space와 Minor GC
New space(Young generation)는 2개의 Semi space로 나뉩니다. 첫 번째 영역은 From space라고 부르며 객체가 생성되고 할당되는 영역입니다. 두 번째 영역은 To space라고 부르며 From space에서 살아남은 객체들이 이동하는 공간입니다.
Minor GC 과정 : 스캐벤저
From space가 꽉 차면 Minor GC는 Mark & sweep알고리즘을 사용해 가비지 컬렉팅을 시작합니다. 이는 아래 Major GC서 자세히 설명하도록하고 여기선 GC 후 영역 이동만 살펴 보겠습니다.
먼저, Minor GC는 From space의 객체들의 참조여부를 확인 후 연결된 곳이 있다면 To space로 객체를 대피시킵니다. 이 때 연속적으로 할당해 메모리 파편화를 방지합니다. 대피가 완료되면 From space에 대피 시키지 않은 쓸모없는 데이터를 제거합니다. 그리고 빈 To space와 From space를 교체해줍니다.
다시 새로운 객체가 할당되고 GC를 할 땐 어떻게 이동할까요?
일단, 새로운 객체는 이번에도 From space에 할당됩니다. 할당될 땐, 기존에 살아남은 객체들 다음 주소를 부여 받고 Minor GC 후 제거 대상이 아니라면 To space로 이동합니다.
그럼 이전 생존 객체들은 어디로 갈까요?
이미 한 번 생존한 객체가 Minor GC에서 또 제외가 된다면 이번엔 To space가 아닌 Old space로 이동합니다. 위 과정을 통틀어 스캐벤저(Scavenger)라고 합니다.
Minor GC와 stop-the-world
스캐벤저, Minor GC를 할 때 쓰레드를 차지하기에 나머지 작업은 중단됩니다. 그렇지만 To space의 용량이 워낙 작고( 1~8MB) helper쓰레드를 병렬적으로 사용해 사용자가 중단을 느끼지 못 할만큼 빨리 처리됩니다.
Old space와 Major GC
여기선 Major GC를 통해 참조여부를 검사합니다. Old space는 이미 gc 검사에서 쓰임이 있다고 판단된 객체들이 존재하기에 Mark-Sweep-Compact 알고리즘과 Tri-color 알고리즘을 통해 더욱 세세히 검사를 합니다. 로직은 크게 3단계로 나눌 수 있습니다.
Major GC 과정
1. 마킹 : DFS로 어떤 객체가 GC 대상인지 알아냄
- DFS로 탐색 후 조건에 따라 Tri-color로 마킹
- 검정: 방문했고 해당 객체가 참조하는 객체가 있는 경우
- 회색: 방문했지만 참조여부는 확인 안한 경우
- 흰색: 아직 탐색 못 함
- 실행 스택과 전역 객체를 담고 있는 객체의 set인 Roots에서 탐색을 시작합니다.
- 시작할 때 모든 객체는 흰색이고 이동 시 회색이 되며 덱에 담깁니다. push_front
- 덱에서 객체를 꺼내 검정으로 마킹하고 해당 객체가 참조하는 객체가 흰색이라면 회색으로 마킹 후 덱에 넣습니다.
- 위 과정을 덱이 빌 때까지 반복합니다. 그러면 다음과 같이 검은색과 흰색만 남습니다.
2. 스위핑 : 주소를 사용가능 하도록
흰색 객체는 참조가 되지 않기에 메모리 해제가 필요합니다. 스위핑 단계에선 free-list자료구조에 흰색 객체의 메모리주소를 기록해 해당 주소들이 사용가능함을 알려줍니다.
3. 압축
재배치를 통화 메모리 단편화 해소 → 추가적인 메모리 확보
Major GC와 stop-the-world
Major GC 역시 Minor GC처럼 병렬적으로 진행됩니다. 이 때 마킹과 스위핑은 Helper 쓰레드에서 처리하고 메인 쓰레드는 압축과 업데이트만 처리해 더 빨리 작업을 끝낼 수 있습니다.
오리노코
위 방식이 도입되기 전 기존 GC는 페이지가 버벅거리고 렌더링 시간을 지연시켰기에 V8은 오리노코라는 프로젝트로 최적의 GC 방식을 찾아내려 했습니다. 다음은 현재 방식이 만들어 지기까지의 과정이기에 참고로만 봐주시면 되겠습니다.
- Parallel 작업을 병렬적으로 수행, 메인 쓰레드의 부담을 줄이기
- Incremental GC
- 메인 쓰레드가 적은 양의 작업을 간헐적으로 처리
대기시간은 줄이지만 총 시간은 같고 오히려 이전 작업이 날라갈 수 있는 단점이 있어서 현재 GC 방식엔 반영되지 않은 것 같습니다.
- 메인 쓰레드가 적은 양의 작업을 간헐적으로 처리
-
Concurrent marking
- 메인 쓰레드는 GC를 하지 않고 헬퍼 쓰레드가 처리
메인쓰레드가 GC에서 벗어나 JS 작업만 처리하기에 중단이 없습니다. 하지만 Helper쓰레드의 GC와 메인 쓰레드의 JS 작업 간 시간 차가 있어서 기존 처리 작업이 무효화가 되거나 오버헤드가 발생하는 문제가 있습니다. 따라서 이 녀석도 현재 방식엔 반영이 되지 않은 것 같습니다.
나를 헷갈리게 했던 부분
클로저 설명: 외부 함수가 종료되서 더이상 유효하지 않을 때도 내부함수가 이를 참조한다면 GC에서 제외된다.
function outer() {
let a = "hello";
return function inner() {
console.log(a);
};
}
let inner = outer();
inner(); // inner 실행 컨텍스트 생성
- 클로저의 원리 : Outer Env에 선언된 시점의 환경정보 바인딩
- 클로저 함수가 호출되면 실행컨텍스트 만들어지고 콜스택에 쌓임 → 원시형 변수 a는 실행 종료 후 제거 → 힙과 상관 없어!
가비지 컬렉터 덕이 아니라 내부함수의 OUTERENV덕이잖아! 왜 생색내!
- 내부함수가 외부함수의 환경정보를 참조할 수 있는 이유는 외부함수가 GC에서 제외되서가 아니라, 내부함수 컨텍스트 OUTERENV에 선언된 당시의 환경레코드가 담겨서다.
실행컨텍스트도, OUTERENV 값도 객체지? 그럼 콜스택에 쌓이고 사라지는 와중에도 실제 값은 HEAP에 있겠네!
결론
참조되지 않는 데이터는 GC된다
라는 건 알았지만 그 대상이 힙영역에 국한 되는 지는 몰랐는데 이번 기회에 정확히 정리를 할 수 있었습니다.
또, 메모리 누수 사례와 메모리 누수를 방지 하는 방법에 대해 공부할 때 어떤식으로 해제가 되는 건지 궁금했는데 Mark-Sweep-Compact, Tri-color 알고리즘을 사용한다는 것을 배웠습니다.
다른 분들도 GC의 과정과 사용하는 알고리즘을 공부해 메모리 누수, 예방에 대해 좀 더 구체적인 모습을 그려볼 수 있었으면 좋겠습니다.
참고
💻 가비지 컬렉션
댓글남기기