가비지 컬렉션은 프로그램에서 더 이상 사용하지 않는 메모리를 자동으로 정리하는 것입니다.
자바스크립트는 두 개의 가비지 콜렉션 방법을 사용합니다.
한 객체를 참조하는 변수의 수슬 추적하는 방법입니다.
객체를 참조하는 변수는 처음에는 특정 메모리에 대해 하나뿐이지만, 변수의 레퍼런스가 복사될 때마다 레퍼런스 카운트가 늘어납니다. 객체를 참조하고 있던 변수의 값이 바뀌거나, 변수 스코프를 벗어나면 레퍼런스 카운트는 줄어듭니다. 레퍼런스 카운트가 0이 되면, 그 객체와 관련한 메모리는 비울 수 있습니다.
한 객체에 flag를 두고, 가비지 컬렉션 사이클마나 flag 표시 후 삭제하는 mark and sweep 방법 입니다. 객체에 in-use flag를 두고, 사이클마다 메모리 관리자가 모든 객체를 추적해서 사용 중인지 아닌지를 표시합니다. 그후 표시되지않은 객체를 삭제하는 단계를 통해 메모리를 해체합니다.
Mark
객체가 생성될 때마다 mark bit 0 (false)로 설정되어집니다. Mark 단계에서 모든 접근 가능한 객체의 mark mit가 1(true)로 설정되어집니다.
Sweep
Mark 단계 후에 mark bit가 여전히 0 (false)로 설정된 객체들은 도달할수 없는 객체이므로 가비지 콜렉터가 수집해 메모리에서 해제됩니다.
let x = {
a: {
b: 2
}
}
let y = x
x = 1
let z = y.a.b
y = 'choongman'
z = null
Marking
1) roots를 모두 회색으로 마킹하고, Deque에 push 합니다.
2) Deque에서 pop front하여 객체를 꺼내어 검은색으로 마킹합니다.(탐색완료)
3) 검은색으로 마킹된 객체가 참조하는 객체들을 회색으로 마킹하고, push front합니다,
4) Deque가 완전히 빌 때까지 b, c를 반복합니다.
5) 최종적으로 검은색과 흰색으로 분류되고 Deque는 완전히 비게딥니다.
이러한 mark-sweep-compaction을 할 경우, heap이 클 경우 한번에 실행하면 수백 밀리초를 소모하게 됩니다.
이를 해결하기위해서 점진적인(incremental)마킹으로 전환했습니다.
incremental : 점진적 마킹을 수행하는 동안 garbage collector는 마킹 작업을 더 작은 덩어리들로 쪼개고 그들 사이사이에 어플리케이션이 실행될 수 있도록합니다.
이렇게 할경우, heap 객체 그래프를 수정했다고 했을 때, GC가 완료되기전에 수정될 수 있다는 문제가 존재합니다. 그래서 이때 'Write Barrier를 이용해 처리를 진행합니다.
객체가 어떤 객체를 참조할 때, Write Barrier를 통해 Old => New 포인터로 정보를 기록하는 것 뿐만 아니라 검은색 객체가 흰색 객체를 참조하는 것도 기록합니다. Marking 부분적으로 진행하면서 검은색=> 흰색 참조를 하는 객체를 발견하면, 처리하지않고 회색으로 변경하여 다시 Marking Worklist에 추가합니다.
Marking Worklist
Major GC의 Marking Queue
Marking bitmap은 atomic 계산으로만 수정되므로 이에 대한 lock은 필요하지 않으며, 이 값을 통해 각 스레드에서 객체의 색을 구분해 처리한다.
만약 어떤 Worker Thread의 Marking Worklist에 객체가 너무 많다면 이를 전역 Marking Worklist에 보내 다른 스레드들이 처리하게 한다.
가비지 콜렉션이 어떻게 동작하는지 알기전에, Heap과 stack에 대해서 알아야합니다
Heap : 객체, 문자열 또는 클로저와 같은 참조 유형을 저장합니다.
참조유형은 객체, string, 클로저 등을 의미합니다.
function Car(arg) {
this.name = arg.name;
}
const McQueen = new Car({ name: 'McQueen'});
이 코드에서 Car
객체는 힙에 생성됩니다.
힙에는 New Space
, Old Space
두 개의 주요 세그먼트를 가지고 있습니다.
Scavenge : 이 가비지 컬렉터는 실행될 때마다 메모리의 작은 부분을 청소하여 young generation을 처리합니다.
Minor GC
라고도 합니다.
New Space
는 새로운 할당이 일어나는 곳입니다. 이 곳에 존재하는 객체를 Young Generation
이라고 부릅니다.
Old Space
는 New Space
에서 GC
로 살아남은 객체들이 이동하게 되어집니다.
Old Space
에서 존재하는 객체를 Old Generation
이라고 합니다.
포인터만 모아놓은 Old pointer space
, 데이터만 모아놓은 Old data space
로 나누어집니다.
Major GC
가 메모리를 관리합니다.
일반적으로 ~20% 가량 Young Generation
이 살아남아 Old Generation
이 됩니다.
포인터
자바스크립트에서 메모리상 포인터와 데이터를 구분하는 방법은Tagged Pointers
방법입니다.
새로운 객체는 메모리가 확보된 To-space에 추가됩니다.
다시 To-space에 메모리가 초과됩니다.
Old space의 객체가 New Space 객체를 참조하고 있는 상황일 때, Minor GC가 발생해서 New Space의 객체가 삭제되면, 해당 객체를 참조하고 있는 Old GC의 객체는 삭제된 메모리의 객체를 참조하는 문제가 발생하게 됩니다.
그래서 어플리케이션에서 참조를 대입하거나 수정할 때, 해당 객체에 대해 '어떤 영역의 객체가 어떤 영역의 객체를 참조한다'는 정보를 기록해두는 방법을 사용하게 되었고, 이를 Write Barrier라고 합니다.
https://hyunjun19.github.io/2018/02/19/node-js-at-scale-node-js-garbage-collection/
https://yceffort.kr/2020/12/javascript-garbage-collection
https://blog.risingstack.com/finding-a-memory-leak-in-node-js/
https://ui.toast.com/weekly-pick/ko_20200228