jaenny.dev

자바스크립트 메모리 구조를 모른다면, 이전 글을 먼저 읽고 와주세요!
https://jaenny-dev.tistory.com/13


V8 엔진이 사용하는 가비지 컬렉터는 generational GC의 일종으로, 객체의 나이를 기준으로 힙 영역을 여러 하위 영역으로 세분화하여 가비지 컬렉션을 수행합니다. V8 엔진이 수행하는 가비지 컬렉션에는 크게 두 단계가 존재합니다.

Minor GC(Scavenger)

Minor GC는 New space 영역에 존재하는 새로 생긴 객체들을 가비지 컬렉트합니다.

New space 영역에선 '할당 포인터'를 사용하여 새로운 객체를 위한 메모리 영역을 할당하는데, 객체가 새로 할당될 때마다 포인터 값이 증가하다가 New space 영역 끝에 다다르면 Minor GC가 수행됩니다. Minor GC는 Cheney 알고리즘을 사용하는데, 꽤 자주 수행되며 별도의 헬퍼 스테드를 이용할 뿐만 아니라 실행 속도 또한 굉장히 빠릅니다.

Minor GC가 수행되는 과정을 살펴보면 다음과 같습니다.

우선, 맨 처음 이미지에서 보았듯이 New space 영역은 To-spaceFrom-space 두 개의 semi-space로 나뉩니다. 항상 Old space에 할당되는 실행 가능한 코드(executable Codes)와 같은 객체를 제외하곤 대부분 From-space에 할당되는데, From-space가 꽉 차게 되면 Minor GC가 실행됩니다.

 

  1. 먼저, 1~6번 객체가 From-space에 존재한다고 하고, 7번 객체를 생성하는 상황이라고 가정합니다.
  2. V8 엔진이 7번 객체를 From-space에 저장하려 하지만, 여유 공간이 없으므로 V8은 Minor GC를 수행합니다.

image

현재 사용되고 있는 객체를 To-space로 옮김. 출처: https://v8.dev/blog/trash-talk

  1. GC 루트(스택 포인터)에서 시작하여 From-space 객체 그래프를 재귀적으로 탐색해가면서 현재 사용중인 객체들을 찾아낸 뒤 이 객체들을 To-space로 옮깁니다. 또한 To-space로 옮겨진 객체가 참조하고 있던 객체들 또한 To-space로 옮겨지고, 이 객체들을 가리키던 포인터도 갱신됩니다. 이 과정이 끝나면 To-space를 압축하여 메모리 단편화(fragmentation)를 줄입니다.
  2. To-space로 옮겨지지 못하고 From-space에 남겨진 객체들은 "가비지"로 취급되어 가비지 컬렉트 됩니다.
  3. To-spaceFrom-space를 맞바꿔서 기존에 To-space로 옮겨진 객체는 다시 From-space에 존재하게 되고, To-space는 비어있게 됩니다. 앞선 과정과 마찬가지로, 새로운 객체는 From-space에 할당됩니다.
  4. 시간이 흘러 From-space에 8번, 9번 객체가 들어온 상태이고, 10번 객체를 새로 할당하는 상황이라고 하겠습니다.
  5. 2.번과 마찬가지로, 10번 객체를 할당할만한 공간이 없기 때문에 V8 엔진은 Minor GC를 다시 수행합니다.
  6. 앞서 살펴본 것과 동일한 과정이 진행되는데, 이때 두 번의 Minor GC 이후에도 살아남은 객체는 Old space로 옮겨집니다.

image

두 번의 Minor GC에도 살아남은 객체는 Old space로 옮겨짐. 출처: https://v8.dev/blog/trash-talk

  1. Minor GC를 수행한 뒤, To-spaceFrom-space를 맞바꿉니다. 그리고 이러한 과정이 반복됩니다.

 

Major GC(Full Mark-Compact)

Major GC는 Old space 영역을 담당하는데, Minor GC에 의해 객체들을 New space에서 Old space로 옮길 때 Old space의 여유 공간이 부족한 경우 실행됩니다.

 

Minor GC의 경우, 데이터 크기가 작은 경우에 적합하지만, Old space와 같이 크기가 큰 영역에 적용하기엔 메모리 오버헤드가 존재합니다. 따라서 Major GC는 Mark-Compact 알고리즘을 사용하는데, 크게 세 가지 단계로 나뉩니다:

  • Marking : 현재 사용되는 객체를 파악하는 단계입니다. 이때 어떤 객체가 "살아있음"을 판단하는 근거로 GC 루트(스택 포인터)에서 시작하여 해당 객체에 도달할 수 있는 지를 살펴봅니다. 힙 영역을 유향 그래프라고 했을 때, 이 그래프에 DFS를 수행하는 것으로 볼 수 있습니다.
  • Sweeping : Marking 단계에서 표시되지 않는 객체가 사용하던 메모리 공간은 free-list에 저장됩니다. free-list는 탐색하기 쉽도록 크기순으로 세분화되는데, 이후에 메모리를 할당하고자 할 때 free-list에서 적절한 크기의 메모리 공간을 찾아 할당하게 됩니다.
  • Compacting : Sweeping 단계를 수행한 뒤, 필요한 경우 메모리 단편화를 해결하기 위해 메모리 압축 작업을 진행합니다. 살아남은 객체를 현재 압축을 진행하지 않은 다른 메모리 페이지에 복사하는 방식으로 진행하는데, 만약 살아남은 객체가 많다면 객체를 복사하는 오버헤드가 커질 수 있습니다. 따라서 단편화가 그리 심하지 않은 페이지는 Sweeping 단계까지만 수행하고 단편화가 많이 진행된 페이지에만 압축을 진행합니다.

major-gc

Major GC 동작. 출처: https://deepu.tech/memory-management-in-v8/

출처

profile

jaenny.dev

@jaenny.dev

Go Beyond! Front-end developer, jaenny✨