이 글은 가장 많이 사용되는 오픈소스 자바스크립트 엔진인, 구글의 V8 엔진에서 일어나는 가비지 컬렉션에 초점을 맞추어 작성하였습니다.
안녕하세요, 용인불주먹입니다.
요즘 저의 최대 관심사인 자바스크립트의 가비지 컬렉션에 대한 글을 써보려고 합니다.
글에서 사용되는 메모리 관리, 가비지 컬렉션 관련 용어들의 정의는 아래 링크를 참조해주세요.
(https://www.memorymanagement.org/glossary/g.html#term-generational-garbage-collection)
제가 처음 배운 프로그래밍 언어는 C++ 입니다.
malloc, calloc, ralloc... 무슨 놈의 alloc 이 이렇게 많은지,
허구헌날 메모리 익셉션에 좌절했던 지난 날이 떠오릅니다.
제가 다음으로 배운 언어는 자바스크립트입니다.
자바스크립트를 배울 때는 가비지 컬렉션에 대해 인지하지 못하고 있었습니다.
왜 인지를 못했겠습니까? 메모리 관련 에러가 나질 않았기 때문이죠. (아 물론 발생은 하지만 그 빈도가 미미해졌음)
또, 대부분의 자바스크립트 서적에서 가비지 컬렉션에 대한 언급을 찾아보기 어렵기도 합니다.
어느날 저는 우연히 자바스크립트 엔진의 가비지 컬렉션에 대한 글을 읽게 되었고, 보이지 않는 곳에서 제 코드의 메모리 관리를 위해 힘써준 그 녀석에 대한 고마움으로 가슴이 웅장해졌습니다.
제 머릿속은 이내 가비지 컬렉션에 대해 궁금한 점으로 가득 찼습니다. 가비지 컬렉터는 어떤 메모리 영역을 가비지로 간주할까? 가비지 컬렉터는 얼마나 자주 동작할까? 가비지 컬렉터는 자바스크립트 코드가 실행되는 스레드와 별도의 스레드에서 동작할까? 등등...
여러 궁금증이 생겨 가비지 컬렉션에 대한 여러 글을 읽어보았고, 마침내는 V8 엔진의 코드까지 보게 되었습니다.
그리고 위 내용들을 찾아보는 동안 자바스크립트 엔진의 동작 방식이나 메모리 관리 방식 등에 대한 깊은 통찰을 얻게 되었고, 유익한 시간을 보냈습니다. 또한 유익함은 나누면 두 배가 되기 때문에 글을 쓰게 되었답니다.
본 편에서는 가비지 컬렉션과 관련한 용어 정리, 그리고 가비지 컬렉션의 대상이 되는 메모리 영역과 가비지 컬렉터가 언제 동작하는지에 대한 궁금증을 풀어보겠습니다.
먼저 매니지드 언어와 언매니지드 언어의 개념입니다.
매니지드 언어란 언어 차원에서 가비지 컬렉션 기능이 제공되는 언어로 자바, 자바스크립트, Go 등이 있습니다.
언매니지드 언어의 예로는 C, C++ 등이 있습니다.
매니지드 언어는 개발자가 메모리를 명시적으로 해제할 필요가 없기 때문에 메모리 누수, 이중 해제, 조기 해제 등 메모리와 관련된 많은 문제들을 방지할 수 있습니다.
한편 개발자가 메모리를 "잘" 관리하여 메모리 최적화가 잘 되었다는 가정하에, 언매니지드 언어는 매니지드 언어보다 높은 퍼포먼스를 얻을 수도 있습니다. 메모리 측면의 퍼포먼스가 개발자의 역량에 더 의존한다고 할 수 있습니다.
먼저 원시 값이 할당되는 스택 영역은 OS 에 의해 관리되기 때문에 가비지 컬렉터의 관리 대상이 아닙니다.
그럼 전체 힙 영역이 가비지 컬렉션의 대상이 되는걸까요?
아닙니다.
조금 더 자세히 알아보기 위해 V8 엔진이 관리하는 힙 영역의 구성에 대해 알아보겠습니다.
V8 엔진은 힙을 세대(generation)로 구분하여 관리합니다.
새로 생성된 객체가 할당되는 new generation 영역과 오래 살아남은 객체가 이동하는 old generation 영역이 있습니다.
new generation 은 두 개의 semi-space 로 구성됩니다. 새로운 객체는 활성 semi-space (nersery) 에 할당되고, new generation 에서 가비지 컬렉션이 일어나면 활성 semi-space 에 존재하는 객체 중 살아남은 객체는 다른 semi-space (intermediate) 로 이동하고, 다른 semi-space 에 존재하는 객체 중 살아남은 객체는 old generation 으로 이동합니다.
즉 new generation 에서 두 번의 가비지 컬렉션이 일어나는 동안 살아남은 객체들이 old generation 으로 이동합니다. (약 20% 의 객체가 이동한다고 합니다.) 객체의 이동은 비용이 비싼 작업이지만, 대부분의 객체는 금방 소멸한다는 가설 하에 V8 엔진은 이러한 세대별 전략을 채택합니다.
참고로, 힙 영역이 new generation, old generation 두 개의 영역으로 구성되는 것은 아닙니다. 이외에도, 다른 영역의 리밋보다 큰 객체들이 저장되는 large object 영역, map space, code space, cell space, property cell space 영역이 존재합니다. 각 영역에 대한 자세한 소개는 생략합니다.
20ms 주기로? 혹은 100ms 주기로..?
V8 엔진에는 Minor GC(Scavenger), Major GC가 있습니다.
Minor GC는 new generation 을 상대적으로 빠른 주기로 청소합니다.
Major GC는 전체 힙을 대상으로 청소합니다.
새로운 객체를 할당해야 하는데, new generation 에 공간이 부족하다면? Minor GC가 청소할 타이밍입니다. 당연한 이야기죠? 공간이 없으면 치워야죠! 이 때 청소에 소요되는 시간은 new generation 에서 살아남은 객체들의 수에 의존합니다. 객체들이 많이 살아남을 수록 new generation 의 다른 semi-space, 혹은 old generation 으로 이동시켜 주어야 하는 객체들이 많아지기 때문입니다.
그럼 Major GC는 언제 일할까요? old generation 에 살아있는 객체들이 휴리스틱하게 계산된 특정 제한을 초과했을 때 청소를 시작합니다. Major GC는 Mark-and-sweep 기법을 사용하는데요, 살아있는 객체를 표시하는 마킹 작업은 살아있는 객체의 수에 의존합니다. 전체 힙에 대해 마킹하는 작업은 대규모 어플리케이션의 경우 100ms 이상의 시간이 소요되는 비용이 비싼 작업입니다. 이 때 메인 스레드가 100ms 동안 멈추는 일을 방지하기 위해 V8 엔진은 전체 마킹 작업을 작은 청크로 나누어 처리한다고 합니다.
V8 엔진은 60 FPS 성능을 달성하기 위해, 큰 가비지 컬렉션 작업을 작은 청크로 나누어 처리합니다. 하지만 웹 어플리케이션이 애니메이션을 렌더링 하는 도중 큰 가비지 컬렉션 작업이 트리거될 수도 있습니다. 이런 케이스를 위해, V8 엔진은 블링크 렌더링 엔진용 작업 스케쥴러를 두어 지연 시간에 민감한 작업의 우선 순위를 조정할 수 있도록 합니다. 작업 스케쥴러는 작업의 우선 순위를 조정할 수 있을 뿐만 아니라, 현재 시스템이 얼마나 바쁜지, 수행해야 할 작업이 무엇이 있는지, 각 작업이 얼마나 긴급한지 등에 대한 정보를 중앙화합니다. 따라서 시스템이 유휴 상태(Idle)일 가능성이 있는 시기와 유휴 상태가 유지될 것으로 예상되는 시간을 추정할 수 있습니다. V8 엔진은 작업 스케쥴러를 사용하여 유휴 상태인 시간 동안 처리할 특수 작업을 예약할 수 있습니다.
이상으로 1편을 마무리 하겠습니다.
2편에서는 Minor GC, Major GC 의 과정과, V8 엔진에서는 무거운 가비지 컬렉션 작업을 어떻게 효율적으로 처리하는지에 대해 알아볼 예정입니다.
잘못된 내용은 코멘트 주시면 감사하겠습니다 :)