앞선 비동기에 대한 포스팅에서,
이벤트 루프와 태스크 큐를 통한 비동기의 이해를 위해 잠깐 자바스크립트 엔진이 등장했었다.
이번에는 자바스크립트 엔진을 좀 더 자세하게 이해해보자.
먼저 stack, heap 메모리의 차이를 알아야 한다.
스택
스택은 함수 호출 시마다 지역 변수, 매개변수, 리턴값 등이 쌓이는 공간이다.
힙
힙은 동적으로 할당되는 메모리 공간이다.
malloc, new 키워드 등으로 할당이 되며,
힙 영역에 할당한 메모리 공간에 대한 주소가 참조되는 경우가 많다.
기본적으로 스택과 힙은 같은 공간을 공유하며, 둘 사이에 미사용 메모리 공간이 존재한다.
스택과 힙은 두 공간을 공유하므로, 언젠가는 서로 충돌할 수도 있다.
-> 이 때 스택에 의한 충돌은 스택 오버플로우, 힙에 의한 충돌은 힙 오버플로우 라고 한다.
각각의 주된 원인으로는
스택 오버플로 - 함수의 재귀적 호출
힙 오버플로 - 과도한 동적 메모리 할당(malloc) 이 있겠다.
또 비슷한 용어로 버퍼 오버플로가 있는데,
이는 제한된 메모리를, 컴퓨터가 너무 많이 사용해서 화면이 정지되는 현상이다.
V8 엔진?
V8 엔진은 nodeJS의 기반이 되는 엔진이다.
자바스크립트는 인터프리터 언어이므로, 코드를 해석 + 실행할 수 있는 엔진이 필요하다.
여기서 google chrome V8 엔진이 js코드를 해석하고 컴파일해서 기계어로 변환해주는 역할을 한다.
앞서 비동기 포스팅에서 알아봤듯이, 자바스크립트는 단일스레드로 동작하는데 이벤트 루프덕에 여러 작업 처리가 가능했다.
v8은 자바스크립트 컨텍스트 당 한개의 프로세스를 사용하며
실행중인 프로그램을 v8 프로세스에서 할당된 일정량의 메모리로 표현할 수가 있다.
이 메모리는 Resident Set이라고 부르며,
다음의 그림과 같은 내부 구조를 보인다.
여기서 눈에 띄는 것은 커다란 Heap 공간과 Stack 공간이 있으며, Heap에 대해 자세하게 다루고 있다.
그림을 참고하면서 각각 공간들에 대해서 알아보자.
V8에서의 스택 메모리
사실 이 포스팅은 거의 힙 공간에 대한 포스팅이므로, 스택에 대해선 가볍게 다룰 예정이다.
스택은 자동 관리되며, 운영체제가 관리한다.
스택에 대해서는 위의 링크에 들어가서 실행해보길 바란다.
다음으로 알 수 있는 점을 적어보면,
- 전역 스코프는 스택에서 '전역 프레임'에 보관된다.
- 모든 함수 호출은 '프레임 블록'으로 스택 메모리에 추가된다.
- 반환값과 인자를 포함한 모든 '지역변수'들은 스택에서 '함수 프레임 블록' 안에 저장된다.
- int, string 등의 모든 원시타입 값은 스택에 바로 저장된다.
- 객체 타입 값은 힙에 생성되며, 스택 포인터에 의해 힙에서 스택을 참조한다.
- 함수 내에서 다른 함수가 호출된다면, 스택의 최상단에 추가된다.
- 함수가 종료(함수 프레임이 반환)될 때는 스택에서 제거된다.
- 주요 프로세스가 완료되면, 힙에 있는 객체들은 스택 포인터 없이 혼자 남게 된다.
- 해당 객체들은 결국 가비지 컬렉터에 의해 삭제될 것이다.
V8에서의 힙 메모리
힙에는 객체와 동적 데이터들이 저장된다.
Resident set의 그림에서 볼 수 있듯이, 메모리 영역 중 가장 큰 블록이다.
힙은 스택과는 달리 메모리가 자동으로 관리되지 않으므로,
프로그램의 메모리가 기하급수적으로 증가할 위험이 있다.
힙에서는 메모리 관리를 위해 가비지 컬렉션이 발생하며 New, Old 영역에서만 실행된다.
이 가비지컬렉션에 대해서는 각 영역에서 흐름과 관련되어 먼저 영역을 학습하고 다룰 것이다.
힙 메모리는 New, Old, Large Object, Code, [cell, property cell, Map] 영역으로 나뉘며, 차례대로 살펴보자.
New 영역(Young Generation)
새로 만들어진 모든 객체
가 저장된다.
New 영역은 짧은 주기를 가진다.
크기가 작고, JVM S0, S1와 같은 semi 영역
을 가지며,스캐벤져(minor GC)
라는 가비지 컬렉션이 이 영역을 관리한다.
Old 영역(Old Generation)
스캐벤져(minor GC)
가 2번 발생할 때 까지 살아남은 New 영역 객체
들이 이동하는 영역이다.
Old 영역은 메이저GC
가 관리하며, 두 영역으로 나뉜다.
1. Old 포인터 영역
스캐벤져로부터 살아남은 객체
를 가지고 있고, 이 객체들은 다른 객체를 참조
한다.
2. Old 데이터 영역데이터만 가진 객체
(다른 객체 참조x)를 가진다.문자열, boxing된 숫자, doubled unboxing된 배열
등이 이 곳에 위치한다.
Large Object 영역
다른 영역의 제한된 크기보다 큰 객체
가 존재하는 영역이다.
각 객체는 자체 mmap 메모리 영역을 가지며, 가비지컬렉션에 의해 삭제되지 않는다.
mmap(memory map) 메모리 영역이란?
프로그래밍시 디스크에 존재하는 데이터를 가져올 때의 시간을 줄이기 위해,
이 데이터(파일)이 메모리에 위치할 수 있도록 page 개념을 활용한 방식.
프로세스 가상 메모리 주소 공간에 파일을 매핑, 가상 메모리 주소에 직접 접근해서 읽기/쓰기를 수행한다고 한다.
Code 영역(JIT)
컴파일러가 컴파일된 코드를 저장
하는 곳
유일하게 실행 가능한 메모리
가 있는 영역이다.
셀, 속성 셀, 맵 영역
각각 Cells, PropertyCells, Maps
를 포함한다.
각 영역은 모두 같은 크기 객체 포함, 어떤 종류의 객체를 참조하는 지 제약을 두므로 수집을 단순하게 한다.
각 영역은 페이지들로 구성
되어 있고, 페이지 크기
는 라지 오브젝트 영역을 제외하곤 1MB
이다.
가비지 컬렉션
스택은 알아서 관리되지만, 힙 공간은 v8엔진이 관리해야 한다.
이 관리의 역할을 가비지 컬렉션이 수행한다.
가비지 컬렉션은 참조가 없는 객체들이 사용하는 메모리를 비워 새 객체를 위한 공간을 만든다.
참조가 없는 객체란?
더이상 필요가 없어서 스택으로부터 주소 참조가 발생하지 않는 객체이며,
도달 가능성(reachability)이 없는 객체라고도 한다.
태생부터 도달이 가능한 객체로는,
현재 실행중인 함수의 지역 변수,
매개 변수중첩 함수의 체인에 있는 함수에서 사용되는 지역 변수
매개변수전역 변수 등등이 있다.
프로그램이 사용 가능한 것보다 더 많은 메모리가 힙에 할당될 때 메모리 부족 오류
발생한다.
힙이 잘못 관리되면 메모리 누수 위험
이 있기 때문에, 가비지컬렉션이 잘 관리해서 이를 방지해야 한다.
코드로 살펴보는 가비지 컬렉션
가비지 컬렉션은 자바스크립트 엔진 내에서 끊임없이 동작한다.
모든 객체를 모니터링하며, 도달할 수 없는 객체를 삭제하는 방식이다.
우선 간단한 예시들을 통해 알아보자.
객체를 생성하기
let user = {
name : 'Tom'
};
위의 코드를 통해 객체를 생성하면, 힙에 생성된 객체의 위치를 user 변수가 기억하고 있다.
user = null;
user변수의 값을 다른 값으로 덮어쓰면, 기존 { name : 'Tom' } 객체로의 참조가 사라진다.
이렇게 되면 이제 해당 객체는, 도달할 수 없는 상태이다.
사용자는 이 객에 접근할 수도 없으며, 참조할 수도 없다.
이 상태의 객체는 가비지컬렉션의 대상이 된다.
두 개의 변수를 이용해 참조한다면?
let user = {
name : 'Tom'
};
let manager = user;
여기서는 객체의 위치를 user가 기억하고 있으며, manager 역시 그 위치를 기억하고 있다.
그러므로 user, manager 모두 객체의 위치를 알고 있으므로
user = null;
다음과 같이 user를 다른 값으로 덮어 씌워도 manager가 여전히 객체의 주소를 기억하므로
가비지컬렉션의 대상이 되지 않는다.
그림을 통해서 이해하기
다음 그림으로부터 가비지컬렉션의 대상을 알기 위해선, 루트로부터의 도달 가능성을 봐야 한다.
루트로부터 도달 가능한 객체가 아닌 Object3은 가비지 컬렉션의 대상이 된다.
여기서도 마찬가지다.
Object 1, 3, 4는 서로 도달할 수 있지만 문제는 루트에서 이 객체들에 도달할 수가 없다.
그러므로 Object 1, 3, 4 모두 가비지컬렉션의 대상이 된다.
v8의 가비지컬렉션과 발생 과정
앞서 가비지컬렉션이 New, Old 영역에서 이루어진다고 했는데, 각각을 마이너GC(스캐벤져), 메이저GC라고 한다.
스캐벤져(Minor Garbage Collection)
힙의 new 영역에 존재하면서 메모리를 관리한다.
보통 new 영역의 객체들은 1~8MB정도
의 작은 크기이며,
new 영역에 할당 비용은 매우 저렴
하다.
new 영역에서는 객체를 위한 공간을 확보하려 할 때마다 증가
하는 할당 포인터
가 있으며
이 할당 포인터
가 new 영역 마지막에 도달
하면 스캐벤져
가 발생한다.
스캐벤져는 Cheney 알고리즘을 사용해 구현되었는데,
해당 알고리즘의 특징으로는 '매우 자주 발생'하며, '수행속도가 빠르고' '병렬 헬퍼 스레드 사용한다' 정도가 있다고 한다.
스캐벤져(Minor GC) 과정
new 영역은 To 영역
과 From 영역
으로 나뉜다.
다음부터 스캐벤져(minor GC)를 MG로 표현하겠다.
스캐벤져 발생 과정 요약
From 영역에 객체 obj01~06이 존재하고 새 객체 obj07를 생성해야 할 때,
v8이 to 영역
에서 필요한 메모리를 할당할 수 없는 상황이라면 MG가 발생한다.
- MG
에 의해 모든 객체들이 from->to 영역
으로 이동한다.
- MG
가 스택포인터
부터 from 영역
까지 순회하며
- 메모리를 사용한 객체 찾고, 이 객체들만 to 영역으로 이동한다.
- 마지막 객체까지 찾으면, to 영역은 자동 압축되며
조각화로 공간을 더 확보하게 된다.
- 그리고 from 영역에 남은 객체
는 가비지컬렉션에 의해
제거된다.
그리고 새 객체 b07
이 영역
에 할당된다.
일정 시간 후에, 다시 to 영역이 가득 차서 새 객체 obj10 을 할당할 수 없다면
MG가 발생한다.
- 이번에는 두번째 MG 발생이다.
- 다음의 두번째 MG 까지 살아남은 객체들은 Old 영역
으로 이동하게 된다.
- 첫번째 MG 이후 생성되어처음으로 생존한 객체
들은 동일하게 to 영역
으로 이동하게 되고,
- 남아있는 From 영역 객체
는 가비지컬렉션에 의해 제거된다.
MG
는 stop-the-world 프로세스
를 취하지만, 굉장히 빠르고 효율적이라 무시가 가능하다고 한다.
Major GC
Old 영역을 작고 깨끗하게 유지하는 가비지컬렉션이다.
v8에서 Old 영역 메모리가 충분하지 않다 판단
할 때 발생하며
Old 영역은 동적으로 계산된 크기에 기반하며
, Minor GC 주기에서 채워진다.
major GC는 메모리 오버헤드가 있기 때문에 mark-sweep-compact 알고리즘을 사용하는데,
TRI-color(흰색, 회색, 검은색) 마킹 시스템을 이용한다.
해당 시스템은 세 단계 프로세스
를 거치며, 세번째 단계는 조각화 휴리스틱에 따라 실행된다.
메모리 오버헤드란?
어떤 처리를 하기 위해 들어가는 간접적인 처리 시간, 메모리를 의미한다.
예를 들면, A를 위해 단순 실행이 10초일 때, 안전성을 위한 옵션을 추가해 15초가 걸렸다면?
오버헤드는 5초이다.
Mark-Sweep-Compact 알고리즘의 3단계 프로세스
마킹
가비지 컬렉터
가 어떤 객체가 사용중인지 식별
한다.
사용중인 객체는 활성 상태
로 표시하며,힙 메모리
를 방향그래프
로 간주해 DFS를 수행
한다.
즉 해당 객체로부터 접근 가능한 모든 객체들을 방문하게 되며, 이것들에 '마킹'을 진행한다.
스위핑
가비지 컬렉터
가 힙 메모리를 순회
하면서, 활성상태가 아닌 객체의 메모리 주소 기록
한다.
이 공간은 free-list
에 사용 가능한 목록
으로 표시되며, 이 목록에 있는 공간에는 다른 객체의 저장이 가능하다.
(즉 마킹되지 않은 모든 객체들이 메모리에서 삭제된다고 이해할 수 있다.)
압축
스위핑 후 필요하다면, 모든 활성 객체들이 함께 이동한다.조각화를 줄이고
, 새 객체들에 대한 메모리 할당 성능 증가
효과가 나타난다.
Major GC는 수행 중에 앱 실행을 멈추므로, stop the world GC
라고도 한다.
Major GC 과정
많은 Minor GC 주기를 거치고 Old 영역이 거의 다 차게 되면,
V8이 Major GC를 발생시킬 것이다.
- Major GC는 스택 포인터에서 시작한다.
- 재귀적으로 객체 그래프를 순회하며, Old 영역 내 메모리를 사용한 객체와 남아있는 객체를 가비지로 표시한다.
- 동시 마킹이 완료되거나 메모리 제한에 도달하면 Major GC가 메인 스레드를 사용하여 마킹의 마지막 단계를 수행한다.
- 이 때 일시 정지 시간이 발생한다.
- Major GC는 동시 스위프 스레드를 사용해 모든 참조 없는 객체들의 메모리를 사용 가능한 상태로 표시한다.
- 또한 조각화를 피하기 위해 관련 메모리 블록을 동일한 페이지로 이동시키는 병렬 압축 작업도 발생한다.
그리고, 스택 포인터들은 이 세 단계를 통해 갱신된다.
마치며
V8 엔진의 메모리와 관리 방법에 대해 학습하면서
스택 힙 등의 메모리와 가비지 컬렉션이 동작하는 원리에 대해 알 수 있었다.
또한 예전 C언어에서 malloc으로 할당한 변수들을 해제해줬던 작업들과
최근 클로저를 공부하면서 '클로저의 남용이 메모리의 문제와 연결된다.'는 이유를 더 잘 이해할 수 있었던 점도 좋았다.
이와 같이, 이전의 파편같은 지식들이 퍼즐같이 맞춰지는 과정이 새로운 지식에 대한 학습의 즐거움인 것 같다.
참고 출처
https://ui.toast.com/weekly-pick/ko_20200228
https://ko.javascript.info/garbage-collection#ref-417
'JavaScript > theory' 카테고리의 다른 글
객체 프로퍼티 플래그와 설명자(Flag & Descriptor) (0) | 2022.01.17 |
---|---|
래퍼 객체(Wrapper Object)란? (0) | 2022.01.14 |
[자바스크립트] 일반 객체 다루기 (0) | 2022.01.11 |
[자바스크립트] 고차함수와 배열 내장 메서드 (0) | 2022.01.06 |
[함수형] 클로저 Closure에 대해 알아보자. (0) | 2022.01.06 |