
해당 글은 프론트 개발자의 기본기 다지기의 세번째 글입니다.
오늘은 javascript 엔진중 V8의 GC가 움직이는 방법에 대해 알아볼 것이다.
먼저 메모리를 왜 배워야 하는가?
컴퓨터의 메모리는 제한이 있는 공간으로 당연히 효율적으로 사용을 해야한다.
만약, 메모리 공간이 부족하게 될 경우 컴퓨터가 급격하게 느려지는 것을 확인 할 수 있게 된다. 무엇보다 요즘 메모리가 금값이다
따라서 메모리 사용량이 너무 많은 프로그램 또는 사이트의 경우 사용자들이 삭제하거나 접속을 꺼릴 가능성이 높아진다.
그렇지 않다.
또한 동적 할당과 해제를 하는 과정은 대부분 많은 시간을 소요하게 됨으로 관리를 하지 않으면 프로그램이 매우 느려지거나 렉이 걸리는것 처럼 버벅일수도 있다.
그렇게 버벅이는 프로그램이 사용자의 컴퓨터가 아닌 나 또는 우리의 컴퓨터에서 돌아가는 경우도 있다. 예를들면? 서버라던가...
그렇다면 당연이 동접 가능한 사용자가 줄어들고 더 많은 서버 비용을 지출하게 될 것이다.
내가 봤을때(내가 아는) 매모리를 관리하는 방법론은 크게 3가지.
각각의 장단점이 존재하는데. 일단은 java script의 GC를 공부하는 과정이니 그중 GC에 대해 알아보자.
일단 저번에 배웠던 메모리 구조이다.

이중 GC가 관리하는 공간은 New Space, Old Space라고 저번시간에 잠깐 언급하고 넘어갔다.
이제 구체적으로 어떻게 해당 개체들이 관리 되는지 알아보도록 하자.
먼저 작은 사이즈의 개체들이 생성된다면 먼저 new space에 생성되게 된다.
new space의 공간은 1~8MB사이다. 따라서, 1MB이상의 개체들은 large Object space에서 관리 된다.
이때 new space는 2가지 공간으로 나눠줘 있는데 하나는 from 하나는 to라고 불린다.
이름이 저렇게 생겨먹은 이유는 곧 알게 된다.
개체들은 2개의 공간중 하나에 공간(from)에 먼저 쌓이게 된다. 이 공간에 차곡차곡 쌓이게 되는데 이땐 처음으로 쌓이니 동적 할당에 대해 시간이 많이 걸리지 않는다.
세미 공간은 스택으로 되어 있기에 동적 할당시에 지금 당장 어디서부터 할당 되어야 하는지에 대한 포인터를 가지고 있다. 따라서 동적 할당의 경우 매우 빠르게 작동 할 수 있다.
C 친구와 다르게..
그러게 포인터를 움직이며 메모리를 쌓다가 공간이 꽉 차게 되면? 붐! 마이너 GC가 작동하게 된다.
마이너 GC는 stack에 있는 변수 리스트를 쭉 불러온다. 그 뒤에 깊은 탐색(번외 - 넓이 우선 탐색 / 깊이 우선 탐색)으로 방문한 개체들을 to space로 보낸다. 그렇게 탐색이 끝나게 됬다면, from 공간에 남아있는 메모리는 지우면 된다.
사실 지울 필요도 없다. 메모리 주소만 처음 값으로 초기화 해주면 된다.
이유는 자료구조 - 스택에서 배울 수 있다.
이렇게 java script GC는 두개의 공간에서 메모리들을 옮기는 과정을 거친다. 이런 과정을 하는 이유는 무엇인가?
1번의 개체들이 사라지는 것이 큰 이유중 하나이다. 매모리를 무한정 쓸 수 없으니 탐색을 하여 필요한 것들만 다음 공간에 냅두고 쓰지 않는 것은 한번에 쓸어버리는 것이다.
그렇다고해서 1번의 이유만으로 이동 시키는 것은 아니다. 사실 그냥 체크만 해두고 나중에 지워도 되기 떄문이다.
2번에서 그 이유를 알 수 있다. 보통 하나의 개체는 연속된 메모리 공간에 들어간다.
꼭 그래야 하는 것은 아니지만, 속도와 메모리 공간의 절약을 위해 그렇게 한다.
그렇지만 잦은 할당과 해제를 하게 되면 메모리에 구멍이 생기게 되고, 새로운 개체에게 딱 맞는 비어있는 메모리 공간을 찾기란 쉽지 않다.
정확하게는 쉽긴 하다. 시간이 많이 걸려서 그렇지..
그렇기에 메모리의 파편화를 없애기 위해 메모리 정리를 하여 동적할당을 쉽게 하려고 하는 것이다.
마이너 GC가 도는 동안은 모든 실행이 멈추며 이때 모든 v8 쓰레드가 GC에 사용된다.
따라서 GC가 도는 동안 사용자는 "렉"을 체험하게 된다.
병렬로 처리하지 않고 모든 쓰레드를 GC에 사용하는 이유는 GC를 안하고 다른일을 하게 될 경우 메모리 정리로 인한 메모리 주소 변경에 따른 오류가 발생 할 수 있음으로 다른 작업들을 멈추고 해당 일을 빠르게 처리하기 위해 노력한다.
하지만 그럼에도 대부분 멀티쓰레드가 가지고 있는 동기화 (동시 접근 금지)작업에 오버헤드가 걸리는건 어쩔수 없는 문제이다.
나중에 배우겠지만 javascript는 동적 타입이라서 실행중에 메모리의 Offset의 주소가 변경된다. 그 때문에 매번 Offset정보를 각 개체마다 업데이트 해줘야 하며 그렇게 생성된 정보를 기반으로 깊은 탐색을 해야 하는데 해당 과정을 V8엔진은 Remember Set이라는 자료구조로 해결한다.
메이저 GC (= old space)도 전반적으로 도는 방법(깊은 탐색을 해서 지우는 방법이)은 비슷하다.
메모리가 공간이 너무 많은 것 때문에 2개의 공간을 만들어 옮기는 것은 큰 무리다. 사용하지 않는 개체야 깊은 탐색으로 정리한다 치고, 파편화는 어떻게 해야 할까?
방법은 페이징 알고리즘이다.
해당 방법을 이용하여 파편화가 일어난 공간은 다른 페이지로 이동하며 정리한다. 이러한 방법을 하게 되면 최소 단위에 페이징 공간만 오버헤드가 되며, 한번에 움직이는 양도 적어진다.
페이징 알고리즘에 대한 설명도 한편이 가능하니, 해당 내용은 검색을 해보자

new space영역에 있는 obejct를 old space영역에 있는 개체가 참조하고 있을 때,
new space개채를 stack에서 참조하지 않는다면 new space개체는 동적할당 해제가 되게 된다.
이럴 경우 old개체가 가지고 있는 주소가 쓰래기가 되게 되며 old개체가 주소를 가지고 있다는 것은 사용한는 개체란 이야기지만, 데이터가 날라가는 문제가 생기게 된다.
그렇다고 해서 new 공간이 다 찼을때 old 공간까지 탐색 하는 것은 너무나 많은 오버헤드
이러한 문제를 해결하기 위해서 Write Barrier를 사용하는데

이것은 어떤 개체가 어떠한 정보를 참조하고 있는지 적어둔 것으로 매번 개체들에 대해 탐색을 하는 것이 아닌 화이트 리스트를 적어두고 그때 그때 수정하는 것이다.
V8에서 돌고 있는 쓰레드가 몇개이든 메이저 GC가 돌때는 모든 쓰래드가 동일하게 작동한다.
따라서 하나의 쓰레드라고 생각해도 되는데 이때 아주 그냥 평범하게 GC를 하면 다음과 같은 이미지가 되게 된다.

이렇게 되면, GC가 실행하는 동안 너무 많은 프로그램 중단 시간이 생기게 된다.
그래서 프로그램이 멈주지 않는것 처럼 보이면서도 GC의 시간을 보장하는 방법으로, GC일과 JS 실행을 다음 그림과 같이 번갈아 하게 되면 어떻게 될까?

안탑깝게도 해당 방법에도 문제가 있다. GC작업이 다 끝나지 않아서 메모리의 정리가 끝나지 않았는데 해당 메모리를 사용하려고 하거나 새로 할당 하려고 한다면 문제가 발생하게 된다.
예로, 첫번쨰 GC 작업 시간에 사용하지 않겠다고 적어논 개체를 바로 다음 다음 JS 실행 시간때 사용한다면?...
해당 문제의 해결 방법은 어디까지 어떻게 작업 했는지 기록하는 것이다.
기록은 다음과 같은 순서로 한다.
이중 마킹은 다시 3개의 마킹이 있는데
이는 기본적으로 DFS로 수행되며, Deque를 이용하고 탐색에서는 Stack 형태로 사용한다.
작동 순서는 다음과 같다.
1. Marking Deque에서 Pop_front()를 하여 객체를 꺼낸다.
2. 꺼낸 객체를 검은색으로 마킹한다.
3. 인접 객체를 회색으로 마킹한 후, Marking Deque에 Push_front()를 한다.
이때, Write Barrier에 old → new 개체 참조 뿐만 아니라 검은색 → 흰색 개체 참조도 적어둔다. 이렇게 되면 GC가 작동되기 시작할때 적어둔 검은색 개체들을 회색으로 변경한 뒤에 작동하면 된다.
또한 메이저 GC는 GC를 수행하는 동안 애플리케이션 실행을 멈추므로 stop-the-world GC라고도 한다. 이를 피하기 위해 V8에서는 다음과 같은 기술을 사용한다.
