-
JavaScript Garbage CollectorJavaScript 2021. 1. 7. 14:00
1] 메모리의 생명주기
메모리의 생명주기는 크게 할당, 사용, 해제로 구분할 수 있다.
1. 변수를 사용하기 위해서 메모리를 할당받는다.
2. 메모리를 사용한다.
3. 변수 등의 사용이 끝나면 메모리를 해제한다.
3번에서 메모리를 하지 않으면 메모리 누수가 일어나 컴퓨터가 메모리가 부족하다며 멈추거나 할 수 있다.
자바스크립트에서는 메모리를 할당받고 사용하는 것까진 있지만 메모리를 해제하는 API는 없다.
그래서 사용되는 것이 Garbage Collector(GC)라는 것이다.
2] Garbage Collector란?
Garbage Collector 라는 것은 브라우저(자바스크립트 엔진: 자바스크립트 코드를 실행하는 프로그램 혹은 인터프리터)에서 자동적으로 메모리를 관리하기 위해서 동작되는 것이다.
C, C++ 같이 수동적으로 메모리를 관리해줘야 하는 언어(managed language)는 개발자가 직접 메모리를 관리하지만
JavaScript, Java 등 개발자가 메모리를 직접 관리해주지 않는 언어(unmanaged language)는 GC를 통해서 자동적으로 메모리가 할당, 사용, 해제된다.
자바스크립트에서 GC는 자바스크립트 엔진이 자동적으로 수행하기 때문에 개발자가 강제적으로 GC를 실행하거나 중단할 수 없다.
3] GC의 기준
브라우저는 도달 가능한 상태에 있는 객체를 모두 파악하고 도달이 불가능하면 메모리를 해제한다.
1) 루트
- 전역변수
- 현재 함수의 지역변수와 매개변수
- 중첩 함수의 체인에 있는 함수에서 사용되는 변수와 매개변수
- 기타 등등
- 이러한 값들은 도달이 가능하기 때문에 명백한 이유없이는 메모리에서 삭제되지 않는다.
2) 도달 가능한 객체
- 루트가 참조가능한 값
- 체이닝으로 루트에서 참조가능한 값
3) 도달가능한 상태
- 도달가능한 상태는 외부에서 들어오는 참조만이 영향을 준다.
- 외부로 나가는 참조는 영향을 주지 않는다.
이외의 것들은 도달 불가능하다고 볼 수 있다.
또한 참조된다고 해서 도달가능한 것은 아니며 서로 연결된 객체들도 도달 불가능할 수 있다.
하지만 문맥적으로 파악해야 하는 것도 있으니 주의하자.
4] GC에서 사용되는 알고리즘
대표적으로 mark-and-sweep 알고리즘이 있다.
이 알고리즘의 동작과정은 다음과 같다.
1. 루트에서 정보를 수집하고 기억(mark)한다
2. 루트가 참조하고 있는 모든 객체를 방문하고 mark한다.
3. mark된 모든 객체들이 참조하는 객체도 mark하고 이미 방문한 객체는 다시 방문하지 않는다.
4. 루트에서 도달가능한 객체들을 방문할 때까지 위 과정을 반복하고
mark되지 않은 모든 객체를 메모리에서 삭제(sweep)한다.
5] GC의 최적화 기법
GC를 동작하는 과정에서 앱 실행에 영향을 줄 수 있기 때문에 다음과 같은 3가지 기법으로 최적화를 한다고 한다.
1) generational collection(세대별 수집): 제 역할을 수행하여 쓸모없어진 객체를 새로운 객체로 분류하고 이를 집중적으로 공격하여 메모리에서 삭제한다. 일정 시간동안 살아남은 객체는 오래된 객체로 분류하여 GC에서 덜 감시한다.
2) incremental collection(점진적 수집): 방문해야 할 객체가 많을 때 모든 객체를 한 번에 방문하여 mark하는 것은 많은 시간과 많은 리소스가 소모된다. 이는 실행속도에 영향을 줄 수 있기에 여러 부분으로 나누어서 각 부분을 별도로 실행한다. 변경사항을 추적하는데 추가적인 작업이 필요하지만 긴 지연시간을 짧은 지연시간으로 분산시킬 수 있는 장점이 있다.
3) idle-time collection(유휴 시간 수집): 실행에 주는 영향을 최소화하기 위해 CPU가 idle-time일 때에만 GC를 수행한다.
6] V8 엔진에서 C++로 구현된 고성능 GC (2020.05.26)
@ 이 포스트는 직접 번역하고 이해한 것을 토대로 작성된 것이니 오역이 있을 수 있으니 주의!
@ 중간에 생략된 내용도 있으니 원문을 참고 (링크) , 사진과 내용의 출처는 모두 원문 링크
1) 개발 배경
크로미움 개발팀은 기존의 V8이 DOM 주위의 C++ 객체 그래프가 JavaScript 객체와 너무 많이 얽혀있어서
2년 전부터 Oilpan(오일팬)이라 부르는 새로운 메모리 관리 방법으로 전환했다.
이 오일팬은 mark-sweep 알고리즘으로 구현됐고 동작은 다음 2단계로 구분된다
1} 생존한 객체를 찾아서 heap 메모리에서 marking하여 관리한다.
2} 죽은 객체를 찾으면 heap 메모리에서 sweeping하여 관리한다.
C++는 자바스크리트와 달리 C++객체는 정적 타입이고 런타임에서 표현을 변경할 수 없다.
오일팬은 이를 기반으로 확인하고 관리하며 방문한 패턴에 통해 다른 포인터에 대한 설명을 제공합니다.
(그래프의 edge)
2) 점진적이고 동시 스위핑
1) 일반적인 웹에서의 동작
1. 이벤트 루프에서 dispatching된 작업을 메인스레드에서 실행한다.
2. 렌더러는 메인 스레드 작업을 처리하는데 도움되도록 메인 스레드에 동시에 실행되는 백그라운드 작업을 지원한다.
(백그라운드 작업과 메인스레드 작업을 동시에 처리하여 메인스레드의 작업을 쉽게 처리한다.)
이전의 오일팬은 앱 실행을 중단하는 마지막 단계 부분에서 stop-the-world sweeping을 실행하도록 구현됐었다.
(아래의 사진 설명)
부드럽고 실시간 제약이 있는 앱의 경우에 GC로 처리할 때 지연시간이 결정적인 요소였다.
stop-the-world sweeping이 사용자가 보는 앱을 끊기게 하거나 지연시간을 유발했기 때문이었다.
점진적으로 접근해서 sweeping을 분리하여 메인스레드의 추가 작업으로 위임했다.
응용프로그램의 실행을 방해하지 않고 idle-time에 sweeping이 완벽하게 동작하는게 가장 좋은 경우이다.
내부적으로 sweeping을 페이지 개념에 기반하여 더 작은 단위로 나눴다.
1. 이미 sweeping된 페이지
2. sweeping이 되지 않은 페이지
할당은 이미 sweeping된 페이지만 고려하고 사용가능한 메모리를 관리하는 free list에서 local allocation buffers를 재할당한다.
1. 앱의 free list에서 메모리를 가져오기 위해 첫번째로 이미 sweeping된 메모리를 찾는 것이고
2. 찾았다면 할당에 들어가는 sweeping 알고리즘을 적용하여 sweeping할 페이지에 메모리를 할당한다.
3. 메모리를 찾지 못했다면 운영체제에게 메모리 할당을 요청한다.
그러나 오일팬은 오랫동안 점진적인 sweeping을 사용했지만 앱과 객체 그래프가 계속해서 커져만 가고
그에 따라 sweeping이 앱 성능에 영향을 미치기 시작했다.
커진 implement sweeping을 개선하기 위해서 백그라운드 작업을 이용하여 동시에 메모리를 회수하기로 결정했다.
개선 내용 중 결정한 것에 2가지 불변적인 규칙이 존재하는데,
1. 백그라운드에서 실행하는 sweeper와 데이터와의 race를 배제해야 한다.
2. 앱에 새 객체를 할당하는 것
↓
1. sweeper는 앱에서 도달 불가능한 죽은 메모리만 처리한다.
2. 앱을 더 이상 sweeper에 의해 처리되지 않는 페이지인 이미 sweeping된 페이지에만 할당한다.
이러한 2가지 불변성은 객체와 메모리의 경쟁이 없다는 건 보장하지만 C++에서는 finalizer로 구현된 소멸자에 의존적이다. 그래서 오일팬은 개발자를 지원하고 앱 코드 내의 데이터의 race를 배제하기 위해서 메인스레드 위에서 finalizer가 강제적으로 실행되도록 하고 있다.
이 문제를 해결하기 위해서 메인 스레드에서 객체 finalization을 연기할 수 있다.
구체적으로 finalizer가 있는 객체를 동시에 sweeper로 실행할 때마다 무조건,
실행 중인 앱의 메인 스레드에서 finalization 단계에 따라 구분하여 finalization queue에 넣는다.
(아래의 사진이 동시 sweeping의 전체 흐름이다)
finalizer가 객체의 payload에 모두 접근할 필요가 있을 수도 있으므로
사용 가능한 메모리 목록과 일치하는 해당 메모리를 추가하는 건 finalizer가 실행된 뒤까지 지연된다.
finalizer가 실행되지 않으면 백그라운드 스레드에서 실행중인 sweeper가 즉시 사용가능한 메모리에서 메모리를 회수하여 추가해준다.
3) 도입 결과
백그라운드 sweeping을 통해 메인스레드에서 sweeping 시간이 25%~50%정도 단축되었다고 한다.
[ M77이 이전까지의 GC, M78이 오일팬을 도입한 GC ]
[ 사진 안 보이면 원문링크에서 보자 (링크) ]
GC가 생각보다 복잡한 거였다...
복습 꾸준히 해야겠다
@@@@@
'JavaScript' 카테고리의 다른 글
JavaScript 디자인 패턴 (0) 2021.05.12 함수 호출 방법 (0) 2021.04.25 Map, Set (0) 2021.01.06 브라우저 동작 원리와 Progressive Render, Built-in 객체 (0) 2021.01.06 1급시민, Lexical Scope, 실행컨텍스트, Closure, this, apply, call, bind (0) 2021.01.06