이 영역이 V8이 객체나 다이나믹 데이터를 담아두는 영역이다. 여기는 메모리 영역 중에서 가장 큰 부분을 차지하며, 가비지 컬렉션이 발생하는 곳이다. 전체 힙메모리가 가비지 컬렉팅이 되는 것은 아니며, 오직 New Space와 Old Space만 가비지 컬렉팅의 대상이 된다.
Young generation
이라 불리는 곳이며, 새로운 객체 또는 단기간 유효한 객체들이 존재하는 곳이다. 이 영역은 상대적으로 작고, 두개의 별도 공간인 Semi Space
가 존재한다. 이는 JVM의 S0
S1
과 비슷하다고 볼 수 있다. 이 공간은 Scavenger
이른바 Minor GC
에 의해서 관리된다. 이 사이즈의 영역은 --mini_semi_space_size
와 --max-_semi_space_size
로 조절할 수 있다.Old generation
이라고 불리는 곳이며, new space
에서 minor GC 사이클로부터 살아남은 객체들이 이동하는 곳이다. 이 영역은 Major GC(Mark-Sweep & Mark-Compact)
에 의해서 관리된다. 이 공간의 사이즈는 --initial_old_space_size
와 --max_old_space_size
로 설정할 수 있다. 이 영역은 두개로 나눠진다.
New Space
의 두번의 minor GC Cycle로 부터 살아남는다면 이쪽으로 이동하게 된다.Just In Time(JIT)
컴파일러가 컴파일된 코드 블록을 보관하는 곳이다. 실행가능한 메모리가 존재할 수 있는 유일한 곳이다. (코드의 양이 커져서 Large Object Space
로 가더라도, 여전히 실행 가능하다.)Cells
PropertyCells
Maps
를 가지고 있는다. 각각의 공간에는 모두 동일한 크기의 객체가 포함되어 있으며, 어떤 종류의 객체를 가리킬 수 있는지에 대한 제한이 있기 때문에 수집을 단순화 한다.각각의 공간은 pages의 세트로 구성되어 있다. 여기서 페이지란, 운영체제 mmap에서 할당된 연속적인 메모리 청크를 의미한다. Large Object Space
를 제외하고는, 각각 1MB이다.
스택 메모리 영역으로, V8 프로세스 하나당 한개의 스택을 가지고 있다. 메서드/함수 프레임, 원시 값, 객체를 가르키는 포인터등 정적인 데이터를 보유하고 있는 곳이다. 이 스택 메모리의 크기는 -stack_size
로 결정할 수 있다.
메모리가 어떤 구조로 되어 있는지 알아봤으니, 이제는 중요한 부분 인 프로그램이 실행될 때 각 부분이 어떻게 사용되는지를 알아보자. 아래 예제 코드를 살펴보자.
class Employee {
constructor(name, salary, sales) {
this.name = name
this.salary = salary
this.sales = sales
}
}
const BONUS_PERCENTAGE = 10
function getBonusPercentage(salary) {
const percentage = (salary * BONUS_PERCENTAGE) / 100
return percentage
}
function findEmployeeBonus(salary, noOfSales) {
const bonusPercentage = getBonusPercentage(salary)
const bonus = bonusPercentage * noOfSales
return bonus
}
let john = new Employee('John', 5000, 5)
john.bonus = findEmployeeBonus(john.salary, john.sales)
console.log(john.bonus)
Global Scope
는 스택의 Global Frame
내에 존재한다.frame-block
형태로 추가된다.frame-block
내에 저장된다.보시다시피, 스택은 자동으로 관리되며, 이는 V8이 아닌 운영체재가 수행한다. 따라서 우리는 스택에 대해서 많은 신경을 쓸필요가 없다. 반면 힙은 OS에 의해 자동으로 관리되지 않으며, 메모리 공간도 가장 크고, 동적데이터를 보유하고 있기 때문에 시간이 지남에 따라 프로그램의 메모리가 바닥날 수도 있다. 또한 시간이 지남에 따라 파편화가 되면서 애플리케이션의 속도도 느려질 수 있다. 여기가 바로 가비지 컬렉터가 들어오는 곳이다.
힙의 포인터와 데이터를 구별하는 것은 가비지 컬렉션에서 중요한 부분이며, 이를 위해 V8은 태그된 포인터
라는 접근 방식을 사용한다. 이 방식은 각 단어의 끝에 비트를 표시해두어 포인터인지 데이터인지를 구별한다. 이 접근 방식은 컴파일러지원이 필요하지만서도, 간단하면서도 효율적인 방식이다.
프로그램이 자유롭게 사용할 수 있는 것보다 더 많은 메모리를 힙에 할당하려고 한다면, V8은 메모리 부족 오류를 발생시킨다. 또는 잘못 관리된 힙도 메모리 누수를 이르킬 수 있다.
V8은 가비지 컬렉팅을 활용하여 힙 메모리를 관리한다. 간단히 얘기하자면, 고립된 객체, 즉 더이상 스택에서 직/간접적으로 참조되지 않은 객체들은 메모리에서 해제하며 다른 객체 생성을 위한 메모리 공간을 확보하게 해준다.
V8의 가비지 컬렉터는 V8 프로세스에서 재사용하기 위하여, 사용 중이지 않은 메모리를 화수하는 역할을 한다. V8 가비지 컬렉터는 힙에 있는 객체를 수명별로 분리하여 각각 다른 단계에서 처리한다. 여기에는 두가지 다른 단계가 있고, 3가지 다른 알고리즘을 사용하여 V8에서 가비지 컬렉팅을 한다.
이 GC는 young/new space를 간결하고 깨끗하게 유지하는 역할을 한다. 상대적으로 작은 객체(1~8bm)는 New Space
에 위치하게 된다. New Space
에 있는 비용은 매우 저렴하다. 여기에는 새로운 객체를 위한 공간을 할당하고 싶을 때마다 등가시키는 할당 포인터가 있다. 할당 포인터가 New Space
의 끝에 도달하면, 마이너 GC가 트리거 된다. 이 과정은 Scavenger 라고도 불리우며, 체니의 알고리즘으로 구현되어 있다. 이 과정은 굉장히 빈번하게 발생되며, 병렬로 스레드를 활용해 이루어지기 때문에 굉장히 빠르다.
마이너 GC의 처리과정을 살짝 보자.
앞서 말했듯, New Space
는 두개의 같은 사이즈인 semi-space
로 이루어져 있다. 하나는 to-space
고 다른 하나는 from-space
다. 대부분의 할당은 from-space
에서 이루어진다. (old space
에 할당되는 실행가능한 코드들은 여기에 저장되지 않는다) from-space
가 가득차게 되면 마이너 GC 가 가동된다.
from-space
에 이미 객체가 있다고 가정해보자. (01~06)from-space
로 부터 메모리를 요청하지만, 더이상 객체를 할당할 메모리가 존재하지 않는다. 따라서 V8은 마이너 GC를 트리거 한다.from-space
에서 객체 그래프를 재귀적으로 탐색하여, 사용되거나 살아있는 객체를 찾는다. 이러한 객체는 to-space
로 이동한다. 이러한 객체가 참조하는 모든 객체도 마찬가지로 이동하게 되며, 이들의 포인터 또한 업데이트 된다. 이는 from-space
내의 모든 객체를 모두 스캔할 때 까지 실행된다. 이 작업이 완료되면 to-space
는 자동으로 파편화를 줄이기 위하여 압축된다.from-space
에 남아 있는 객체들은 모두 가비지로 판단하여 비우게 된다.to-space
와 from-space
를 스왑한다. 따라서 모든 객체들은 from-space
에 존재하며, to-space
는 비어 있게 된다.from-space
의 메모리에 할당된다.from-space
에 시간이 흘러서 객체가 더 들어 왔다고 가정해보자.from-space
로 부터 메모리를 요청하지만, 더이상 객체를 할당할 메모리가 존재하지 않는다. 따라서 V8은 마이너 GC를 트리거한다.old-space
로 이동하게 된다. 첫번째 생존자들은 to-space
로 이동하게 되고, from-space
에는 가비지만 남아있고, 이를 비우게 된다.to-space
와 from-space
를 스왑하며, 모든 객체들은 from-space
로 이동하고 to-space
는 비어지게 된다.from-space
에 할당된다.마이너 GC가 어떻게 young generation
에 공간을 요청하고 이를 간결하게 유지하는지 살펴보았다. 이러한 일련의 과정은 프로세스를 중단시키지만, 너무 빠르고 효율적이기 때문에 대부분의 경우 무시할 수 있는 수준이다. 이 프로세스는 nes space
의 참조를 위해 old space
의 객체를 스캔하지 않기 때문에, 이전 space에서 새로운 메모리에 이르는 모든 포인터의 레지스터를 사용한다. 이는 write barrier라고 불리는 과정에 의해 버퍼에 기록된다.
이 GC는 old generation
공간을 간결하고 깨끗하게 유지해준다. V8이 old space
에 더이상 충분한 공간이 없다고 판단했을 때 시작된다.
스캐빈저 알고리즘은 작은 데이터 사이즈에는 매우 완벽하지만, old space
와 같이 힙사이즈가 큰 경우에는 메모리 과부하를 일으킬 수 있어서 메이저 GC에는 Mark-Sweep-Compact
알고리즘을 사용한다. 이 알고리즘은 3색 표시 시스템(흰색, 회색, 검은색) 을 사용한다. 따라서 메이저 GC는 3단계 과정을 거치게 된다.
이러한 유형의 GC는 GC를 수행하는 동안 프로세스의 일시중지를 야기 하기 때문에 stop-the-world
GC라고도 한다. 이를 피하기 위해 V8은 아래와 같은 방법을 사용한다.
Writes Barrier
는 헬퍼가 마킹을 하는 동안 자바스크립트하 생성하는 객체 간에 새로운 참조를 추적하기 위하여 사용된다.Major GC의 프로세스를 살펴보자.
old space
는 거의 가득차서 V8이 메이서 GC를 트리거 했다고 가정해보자.old space
에 있는 사용중인 객체와 가비지를 별개로 표시해둔다. 이 작업은 여러개의 동시 헬퍼 스레드를 사용하여 수행되며, 각 헬퍼는 포인터를 따른다. 이는 주 메인 쓰레드에 영향을 미치지 않는다.