자바스크립트는 웹 브라우저에서 가장 많이 사용되는 프로그래밍 언어입니다.
자바스크립트 코드를 실행하여 웹 페이지의 동적인 기능을 제공하는 인터프리터가 바로 자바스크립트 엔진
입니다.
자바스크립트 엔진에는 크롬의 V8, 사파리의 Webkit, 파이어폭스의 Spider Monkey 등이 있습니다.
오늘 글에서는 개발자가 가장 많이 사용하고 있는 구글 크롬의 V8을 기준으로 자바스크립트 엔진 구성과 동작 원리를 설명합니다.
메모리 관리
자바스크립트 엔진은 실행시간에 메모리 관리를 위해 메모리 힙
과 호출 스택
을 사용합니다.
메모리 힙은 동적으로 할당된 메모리가 저장되는 공간입니다. 자바스크립트에서 객체, 배열, 함수 등의 데이터 타입이 메모리 힙에 할당됩니다. 이 메모리는 자동으로 관리하고 사용하지 않는 메모리를 자동으로 해제합니다.
호출 스택은 함수 호출에 관련된 정보를 저장하는 공간입니다. 함수를 호출하면 호출 스택에 새로운 프레임을 추가합니다. 이 프레임은 함수의 인수, 지역 변수, 반환 주소 등을 저장합니다. 함수 실행을 완료하면 해당 프레임이 호출 스택에서 제거합니다. 따라서 현재 실행 중인 함수가 스택의 맨 위에 위치합니다.
자바스크립트 엔진은 호출 스택을 사용하여 실행 중인 코드의 실행 흐름을 추적합니다.
호출 스택의 크기는 한정되어 있어 너무 많은 함수를 중첩하여 호출하는 경우 스택 오버플로우
가 발생할 수 있습니다.
- 유명한 개발자 포럼 사이트의 이름도 여기서 따왔다고 합니다.
동작 원리
자바스크립트 엔진은 4단계로 작동합니다.
- 파서(Parser) : 어휘 분석(Lexical Analysis) 이라는 과정을 통해 코드를 토큰으로 분해한다.
- 추상 구문 트리(Abstract Syntax Tree, AST) : 파서가 분해한 토큰으로 트리를 생성한다.
- 추상 구문 트리에서 나온 코드를 인터프리터(ignition)에 전달하고 인터프리터는 코드를 바이트 코드(Bytecode)로 변환한다.
- V8 에서는 인터프리터가 고급 언어로 작성된 소스 코드를 가상머신이 편하게 이해할 수 있게 바이트 코드로 컴파일한다.
- Ignition을 인터프리트 방식으로 개발하여 다음 세가지 장점을 가진다.
- 메모리 사용량 감소 : 자바스크립트 코드를 기계어로 컴파일하는 것보다 바이트 코드로 컴파일하는 것이 더 편하다.
- 파싱 시 오버헤드 감소 : 다시 파싱하기 편하다.
- 컴파일 파이프 라인의 복잡성 감소 : 최적화(Optimizing), 최적화 해제(Deoptimizing) 과정에서 바이트 코드만 고려하면 된다.
- 인터프리터가 코드를 실시간으로 변환하면서 브라우저에게 작업을 지시하는 동안 프로파일러가 최적화 할 수 있는 부분을 찾아서 기록한다.
- 최적화가 가능한 부분(자주 사용되는 부분)을 찾으면 프로파일러는 이를 컴파일러에게 전달하고, 컴파일러는 인터프리터에 의해 실시간으로 웹사이트가 구동되는 동안 필요한 부분을 기계어로 변환하여 최적화를 진행한다.
- 최적화한 코드를 수행할 차례가 다가오면, 바이트 코드 대신 컴파일러가 변환한 최적화된 코드가 그 자리를 대체하여 실행된다.
- 프로파일러(Profiler)는 함수나 변수들의 호출 빈도와 같은 매트릭 수집한다. 수집한 매트릭을 TurboFan에게 보내 최적화 한다.
- 사용 빈도가 낮은 코드는 다시 최적화하기도 한다.
- 컴파일러는 프로파일러에게 전달받은 내용을 토대로 기계어로 변환하여 최적화를 진행한다.
V8 엔진은 다음과 같은 조건으로 최적화 한다.
- 코드가 뜨겁고 안정적인 것, 쉽게 말하면 뜨거운은 자주 호출하는 코드이고 안정적은 변하지 않는 것을 의미한다. 대표적으로 매번 같은 행동을 수행하는 반복문 내에 있는 코드가 있다.
- 인터프리팅된 바이트 코드의 길이를 보고 특정 임계점을 넘지 않으면 작은 함수라고 판단해서 최적화를 진행한다. 작고 단순한 함수는 크고 복잡한 함수보다 동작이 매우 추상적이거나 제한적인 확률이 높아 안정적이라고 볼 수 있다.
- 최적화 기법으로는 히든 클래스(Hidden Class)나 인라인 캐싱(Inline Caching) 등 여러가지 기법을 사용한다.
- 최적화한 코드를 수행할 차례가 되면, 바이트 코드 대신 최적화한 코드를 실행한다.
V8 엔진의 최적화 방법
히든 클래스(Hidden Class)와 인라인 캐싱(Inline Caching)을 사용합니다.
히든 클래스
다른 자바스크립트 엔진들이 프로퍼티를 저장하기 위해서 사전식 데이터 구조를 이용하는 반면 V8은 히든 클래스를 이용합니다.
V8은 객체에 새로운 프로퍼티를 추가할 때 히든 클래스를 생성하고 프로퍼티의 정적인 위치인 오프셋(offset)을 저장하여 실제 데이터가 저장되어 이는 위치에 대한 포인터(Pointer)를 제공합니다.
포인터는 런타임 시 직접적인 데이터 접근이 필요하지 않아 위치 정보 해석을 할 필요가 없습니다.
프로퍼티를 새롭게 추가할 때마다 새로운 히든 클래스를 생성하는 방식이 비효율적으로 보이지만 객체를 생성할 때 이전에 생성했던 객체와 같은 것이 존재하면 이미 저장한 히든 클래스를 재사용하여 객체 생성 비용을 줄입니다.
인라인 캐싱
자바스크립트 엔진은 또한 인라인 캐싱을 사용하여 객체에서 프로퍼티를 찾을 수 있는 위치 정보를 제공하여 조회 횟수를 줄입니다. 즉, 객체 접근이 반복적으로 일어나는 경우 엔진이 불필요한 메모리 접근을 최소화하여 최적화합니다.
긴 글 읽어 주셔서 감사합니다.
참조
- https://www.freecodecamp.org/news/javascript-essentials-why-you-should-know-how-the-engine-works-c2cc0d321553/
- https://d2.naver.com/helloworld/59361
- https://velog.io/@seungchan__y/자바스크립트는-Compiler-Interpreter-언어다
- https://joshua1988.github.io/web-development/translation/javascript/how-js-works-inside-engine/
- https://www.google.com/search?q=자바스크립트+엔진+동작+원리&source=lnms&tbm=isch&sa=X&ved=2ahUKEwiJ85Ovju39AhUhmlYBHZ9GAZwQ_AUoAXoECAEQAw&biw=1920&bih=969&dpr=1#imgrc=5iTW4B-CkeC_pM