가비지 컬렉션
Node.js는 V8엔진을 사용한다. V8 engine의 가장 큰 특징은 자동으로 메모리를 관리한다는 것이다. 이것은 가비지 컬렉션이 있어서 가능하다.
가비지 컬렉션은 힙 영역에서 더는 사용되지 않는 객체를 삭제하는 역할을 수행한다. 사실 이전에 자바의 메모리 구조와 가비지 컬렉션 동작 방식을 공부해서 정리한 적이 있었는데, 상당히 비슷한 느낌을 받았다.

V8 엔진의 도식화된 Heap 사진이다.
예전에 자바에서의 가비지 컬렉션을 공부해본 경험이 있다.
위 그림을 보고 자바에도 저렇게 Young 영역과 Old 영역이 있는데, 자바스크립트도 가비지 컬렉터를 자바와 비슷하게 사용하는건가? 라는 생각을 했다. 그리고 찾아보니 거의 비슷했다.
자바는 Young Generation과 OldGeneration 부분을 나누어 에덴 영역(가장 young)에서부터 가비지 콜렉터를 집중적으로 관리하는 방식이다.
V8 엔진의 New space와 Old space
마찬가지로 V8엔진도 New-space라는 젊은 객체들을 관리하면서 점차 Old 영역으로 이동하는 방식이다.

객체는 New space의 Semi space에 할당된다. 그리고 생존하면 New space의 semi space로 이동한다. 또 생존하면 Old Space로 이동한다.
여기서 Semi space가 왜 두 개가 존재할까? 이것은 동작 원리를 이해하면 굉장히 쉬운데 대피를 위한 임시 공간이다!
두 개의 스페이스 중 정리되는 스페이스에서 살아남아야하는 객체들이 이동하고, 정리되는 곳은 완전히 비운다. 이 과정을 반복하는 것이기 때문에 서로 계속해서 역할이 대피소와 삭제할 곳으로 스위칭된다.
그러다가 두 번 생존하면 이제 Old Space로 넘어가게 된다.
왜 New Space와 Old Space 부분으로 나누어 힙을 관리할까?

이렇게 Young한 부분과 Old 부분을 관리하는 이유는, 어린 객체들에게서 힙이 정리될 여지가 크기 때문이다.
위 그림은 객체들의 수명 통계를 그린 그래프인데, 대부분의 객체가 수명이 짧은 것을 알 수 있다. 그래서 힙을 탐색하는 범위를 어린 객체들을 우선으로 탐색해서 가비지 컬렉션을 수행함으로써 불필요하게 매번 전체 힙을 탐색하는 오버 헤드를 줄일 수 있다.
Minor GC, Major GC
minor GC는 New space를 타겟으로 하는 가비지 컬렉터다. 그리고 동작은 위에서 언급한 것처럼 대피소와 저장소를 분리하며 스위칭한다.
major GC는 Old space에 있는 객체들을 타켓으로 한다. 이때 Mark-Sweep-Compact 알고리즘을 사용한다.

Mark and Sweep은 루트 스페이스로 부터 해당 객체에 접근 가능한지를 살펴보는 것이다
루트 스페이스 부터 그래프 순회를 통해 연결된 객체를 찾고, 연결이 끊어진 객체를 지운다. 연결된 객체를 파악하는 것을 Mark, 연결이 없는 객체를 지우는 것을 Sweep이라고 한다. 그리고 Compact를 통해 메모리를 한 곳으로 몰아서 메모리 단편화를 막아 메모리를 추가적으로 확보하게 된다.
이 방식을 사용하면 순환 참조되는 객체들도 모두 지울 수 있다. 자바와 자바 스크립트는 Mark and Sweep 알고리즘을 통해 메모리를 관리한다.
Stop-the-world 문제
하지만 Mark and Sweep 알고리즘도 단점도 있는데, 바로 의도적으로 가비지 컬렉션 동작을 실행해야한다는 것이다.
그런 이유로, 어플리케이션 실행 도중 가비지 컬렉터에게 컴퓨터 자원을 양보해주어야한다. 그런데 가비지 컬렉터가 동작할 때에는 프로그램이 멈추게 되는 문제가 생기고 이것을 Stop-the-world라고 한다.
그래서 어플리케이션 실행과 GC 실행을 병행하기 위해 어려운 최적화 작업이 필요하다. 최적화 작업을 위해 필요한 여러 기술들을 간단히 알아보자
Parallel

메인 쓰레드 외에 헬퍼 쓰레드를 두어 균등하게 일을 처리하는 것이다. 쓰레드간 동기화를 처리해야하는 문제는 있지만, Stop-the-world 시간을 감소시킬 수 있다.
Incremental

작업 중간 중간에 간헐적으로 GC를 실행하는 것이다.
Concurrent

작업과 동시에 GC를 실행한다. Stop-the-world 시간이 전혀 없다. 하지만 기술적으로 동작 중인 JS에서 정리할 힙 객체를 선별하는 것이 어렵다는 문제가 있다.
Idle-time GC

v8은 크롬과 같은 embedder에게 가비지 컬렉션을 유발할 수 있는 메커니즘을 제공하는데, 크롬은 프로그램이 쉬는 free나 idle time을 알 수 있다.
예를 들면, 1초에 60프레임을 제공하는 크롬은 1프레임을 렌더링 하기 위해 약 16ms(1s / 60)가 소모된다. 만약 애니메이션 프레임 렌더링 작업이 16ms 보다 빨리 끝나면, 크롬은 다음 프레임 작업 전까지 가비지 컬렉션을 유발한다.
참고자료
https://v8.dev/blog/trash-talk
https://fe-developers.kakaoent.com/2022/220519-garbage-collection/
'JavaScript > JS를 파헤쳐보자' 카테고리의 다른 글
C10K와 node.js의 비동기 처리 (0) | 2024.08.22 |
---|---|
[JS] 자바스크립트의 비동기 처리 (0) | 2024.07.30 |
Node.js는 완전한 싱글 스레드일까? (0) | 2024.07.26 |
Call Stack과 클로저함수 (0) | 2024.07.24 |
자바스크립트는 컴파일러 언어일까 인터프리터 언어일까? (1) | 2024.07.20 |
가비지 컬렉션
Node.js는 V8엔진을 사용한다. V8 engine의 가장 큰 특징은 자동으로 메모리를 관리한다는 것이다. 이것은 가비지 컬렉션이 있어서 가능하다.
가비지 컬렉션은 힙 영역에서 더는 사용되지 않는 객체를 삭제하는 역할을 수행한다. 사실 이전에 자바의 메모리 구조와 가비지 컬렉션 동작 방식을 공부해서 정리한 적이 있었는데, 상당히 비슷한 느낌을 받았다.

V8 엔진의 도식화된 Heap 사진이다.
예전에 자바에서의 가비지 컬렉션을 공부해본 경험이 있다.
위 그림을 보고 자바에도 저렇게 Young 영역과 Old 영역이 있는데, 자바스크립트도 가비지 컬렉터를 자바와 비슷하게 사용하는건가? 라는 생각을 했다. 그리고 찾아보니 거의 비슷했다.
자바는 Young Generation과 OldGeneration 부분을 나누어 에덴 영역(가장 young)에서부터 가비지 콜렉터를 집중적으로 관리하는 방식이다.
V8 엔진의 New space와 Old space
마찬가지로 V8엔진도 New-space라는 젊은 객체들을 관리하면서 점차 Old 영역으로 이동하는 방식이다.

객체는 New space의 Semi space에 할당된다. 그리고 생존하면 New space의 semi space로 이동한다. 또 생존하면 Old Space로 이동한다.
여기서 Semi space가 왜 두 개가 존재할까? 이것은 동작 원리를 이해하면 굉장히 쉬운데 대피를 위한 임시 공간이다!
두 개의 스페이스 중 정리되는 스페이스에서 살아남아야하는 객체들이 이동하고, 정리되는 곳은 완전히 비운다. 이 과정을 반복하는 것이기 때문에 서로 계속해서 역할이 대피소와 삭제할 곳으로 스위칭된다.
그러다가 두 번 생존하면 이제 Old Space로 넘어가게 된다.
왜 New Space와 Old Space 부분으로 나누어 힙을 관리할까?

이렇게 Young한 부분과 Old 부분을 관리하는 이유는, 어린 객체들에게서 힙이 정리될 여지가 크기 때문이다.
위 그림은 객체들의 수명 통계를 그린 그래프인데, 대부분의 객체가 수명이 짧은 것을 알 수 있다. 그래서 힙을 탐색하는 범위를 어린 객체들을 우선으로 탐색해서 가비지 컬렉션을 수행함으로써 불필요하게 매번 전체 힙을 탐색하는 오버 헤드를 줄일 수 있다.
Minor GC, Major GC
minor GC는 New space를 타겟으로 하는 가비지 컬렉터다. 그리고 동작은 위에서 언급한 것처럼 대피소와 저장소를 분리하며 스위칭한다.
major GC는 Old space에 있는 객체들을 타켓으로 한다. 이때 Mark-Sweep-Compact 알고리즘을 사용한다.

Mark and Sweep은 루트 스페이스로 부터 해당 객체에 접근 가능한지를 살펴보는 것이다
루트 스페이스 부터 그래프 순회를 통해 연결된 객체를 찾고, 연결이 끊어진 객체를 지운다. 연결된 객체를 파악하는 것을 Mark, 연결이 없는 객체를 지우는 것을 Sweep이라고 한다. 그리고 Compact를 통해 메모리를 한 곳으로 몰아서 메모리 단편화를 막아 메모리를 추가적으로 확보하게 된다.
이 방식을 사용하면 순환 참조되는 객체들도 모두 지울 수 있다. 자바와 자바 스크립트는 Mark and Sweep 알고리즘을 통해 메모리를 관리한다.
Stop-the-world 문제
하지만 Mark and Sweep 알고리즘도 단점도 있는데, 바로 의도적으로 가비지 컬렉션 동작을 실행해야한다는 것이다.
그런 이유로, 어플리케이션 실행 도중 가비지 컬렉터에게 컴퓨터 자원을 양보해주어야한다. 그런데 가비지 컬렉터가 동작할 때에는 프로그램이 멈추게 되는 문제가 생기고 이것을 Stop-the-world라고 한다.
그래서 어플리케이션 실행과 GC 실행을 병행하기 위해 어려운 최적화 작업이 필요하다. 최적화 작업을 위해 필요한 여러 기술들을 간단히 알아보자
Parallel

메인 쓰레드 외에 헬퍼 쓰레드를 두어 균등하게 일을 처리하는 것이다. 쓰레드간 동기화를 처리해야하는 문제는 있지만, Stop-the-world 시간을 감소시킬 수 있다.
Incremental

작업 중간 중간에 간헐적으로 GC를 실행하는 것이다.
Concurrent

작업과 동시에 GC를 실행한다. Stop-the-world 시간이 전혀 없다. 하지만 기술적으로 동작 중인 JS에서 정리할 힙 객체를 선별하는 것이 어렵다는 문제가 있다.
Idle-time GC

v8은 크롬과 같은 embedder에게 가비지 컬렉션을 유발할 수 있는 메커니즘을 제공하는데, 크롬은 프로그램이 쉬는 free나 idle time을 알 수 있다.
예를 들면, 1초에 60프레임을 제공하는 크롬은 1프레임을 렌더링 하기 위해 약 16ms(1s / 60)가 소모된다. 만약 애니메이션 프레임 렌더링 작업이 16ms 보다 빨리 끝나면, 크롬은 다음 프레임 작업 전까지 가비지 컬렉션을 유발한다.
참고자료
https://v8.dev/blog/trash-talk
https://fe-developers.kakaoent.com/2022/220519-garbage-collection/
'JavaScript > JS를 파헤쳐보자' 카테고리의 다른 글
C10K와 node.js의 비동기 처리 (0) | 2024.08.22 |
---|---|
[JS] 자바스크립트의 비동기 처리 (0) | 2024.07.30 |
Node.js는 완전한 싱글 스레드일까? (0) | 2024.07.26 |
Call Stack과 클로저함수 (0) | 2024.07.24 |
자바스크립트는 컴파일러 언어일까 인터프리터 언어일까? (1) | 2024.07.20 |