탐색으로 돌아가기
V8 Engine17 / 24 단계

V8 Engine

파싱 · 컴파일 · JIT

크롬·Node의 JS 엔진. JS를 파싱해 바이트코드로 만들고, 자주 쓰는 코드는 JIT로 최적화한다.

V8은 구글이 만든 오픈소스 JavaScript/WebAssembly 엔진으로, Chrome과 Node.js의 실행 코어다. 소스코드는 먼저 스캐너(scanner)가 문자를 토큰(token)으로 분해하고 파서(parser)가 이를 AST(추상 구문 트리)로 만든 뒤, Ignition 인터프리터가 AST를 바이트코드(bytecode)로 컴파일해 즉시 실행한다. 실행 도중 인라인 캐시(inline cache)와 프로파일링으로 '뜨거운(hot)' 함수와 타입 정보를 수집하고, 충분히 자주 실행되면 TurboFan(및 중간 계층 Maglev/Sparkplug)이 이를 근거로 투기적(speculative) 최적화 기계어를 JIT 컴파일한다. 최적화는 '이 객체는 항상 이 히든 클래스(hidden class)'라는 가정에 의존하므로, 가정이 깨지면 최적화 해제(deoptimization, deopt)가 일어나 다시 바이트코드 인터프리터로 되돌아간다. 메모리는 세대별(generational) 가비지 컬렉터가 힙(heap)을 관리하며, 짧게 사는 객체는 Scavenge로, 오래 사는 객체는 Mark-Sweep-Compact로 회수한다.

내부 구성

스캐너 (Scanner)
소스 문자 스트림을 읽어 키워드·식별자·리터럴 등 토큰(token)으로 분해하는 어휘 분석 단계
파서 (Parser) → AST
토큰을 문법 규칙에 따라 추상 구문 트리(AST)로 조립. 지연 파싱(lazy/pre-parsing)으로 당장 실행 안 할 함수는 미룬다
Ignition (인터프리터)
AST를 조밀한 바이트코드로 컴파일하고 이를 해석 실행하는 기본 실행기. 기계어 대비 메모리 사용이 작다
바이트코드 (Bytecode)
Ignition이 실행하는 플랫폼 독립적 중간 표현. 최적화 컴파일러의 입력이자 deopt 시 되돌아갈 기준점
Sparkplug (베이스라인 컴파일러)
바이트코드를 최적화 없이 빠르게 기계어로 변환하는 비최적화 컴파일러. 인터프리터와 TurboFan 사이 지연을 메운다
Maglev (중간 최적화 JIT)
가벼운 타입 정보로 준수한 기계어를 빠르게 생성하는 중간 계층 최적화 컴파일러
TurboFan (최적화 컴파일러/JIT)
런타임 프로파일과 타입 피드백을 근거로 투기적 최적화가 적용된 고성능 기계어를 생성
최적화 해제 (Deoptimization/Deopt)
TurboFan의 투기적 가정(shape·타입 등)이 깨지면 최적화 기계어를 버리고 바이트코드 인터프리터로 안전하게 복귀
히든 클래스 (Hidden Class / Map)
객체의 구조(프로퍼티 이름·순서·오프셋)를 기술. 같은 shape 객체는 Map을 공유해 빠른 비교·접근 가능
디스크립터 배열 (DescriptorArray)
특정 Map에 속한 프로퍼티 목록·위치·속성을 저장. 여러 Map이 부분 공유 가능
트랜지션 트리 (TransitionArray)
프로퍼티 추가 시 어느 Map으로 전이할지 나타내는 간선. 객체 shape 변화를 트리로 추적
인라인 캐시 (Inline Cache, IC)
특정 호출 지점에서 관측된 Map과 프로퍼티 오프셋을 캐싱해 반복 접근을 빠르게 하고, TurboFan에 타입 피드백을 제공
가비지 컬렉터 (Orinoco GC)
도달 불가능한 객체를 회수하는 세대별·대부분 병렬/동시 수집기. jank와 메모리 사용을 최소화
Young Generation (Scavenge / Minor GC)
새로 생성된 단명 객체 영역. Cheney의 반공간(semispace) 복사 알고리즘으로 from→to 복사하며, 첫 scavenge 생존 시 중간 세대로, 두 번째 scavenge 생존 시 old로 승격
Old Generation (Mark-Sweep-Compact / Major GC)
오래 산 객체 영역. 마킹→스위핑→컴팩션 3단계로 회수하고 단편화를 압축으로 해소
힙 (Heap)
객체·배열·함수·클로저 등 동적 할당 데이터가 사는 GC 관리 메모리 영역
스택 (Stack)
함수 호출 프레임·지역 원시값·힙 객체 참조가 쌓이는 LIFO 실행 스택 (콜스택)

핵심 포인트

  • 실행 파이프라인: 소스 → 스캐너(토큰) → 파서(AST) → Ignition 바이트코드 → 실행 → 프로파일 → TurboFan 최적화 기계어(JIT)
  • V8은 순수 인터프리터가 아니라 인터프리터+JIT의 다계층(tiered) 구조: Ignition(인터프리터) → Sparkplug(베이스라인) → Maglev(중간 최적화) → TurboFan(최고 최적화)
  • 히든 클래스(hidden class, 내부 명칭 Map)로 객체 구조를 공유·비교하고, 인라인 캐시(IC)로 반복되는 프로퍼티 접근을 캐싱해 속도를 높인다
  • TurboFan의 최적화는 투기적이라 가정이 깨지면 deopt로 인터프리터에 되돌아간다(예: 객체 shape 변경, 타입 변경)
  • 가비지 컬렉션은 세대별: young generation은 Scavenge(Cheney 반공간 복사), old generation은 Mark-Sweep-Compact
  • young에서 GC를 두 번 견딘(nursery→중간 세대→old) 객체가 old generation으로 승격(promotion)되며, Orinoco 프로젝트로 대부분 병렬·동시(concurrent) 처리해 멈춤(jank)을 줄인다
  • 힙(heap)은 객체·클로저 저장, 스택(stack)은 함수 호출 프레임과 원시값 참조를 저장하는 별개 영역

심화

면접에서 자주 나오는 핵심은 '왜 히든 클래스가 있는가'와 'monomorphic vs polymorphic vs megamorphic IC'다. JavaScript 객체는 동적이지만 V8은 '같은 방식으로 만들어진 객체는 같은 shape을 가진다'고 가정해 C++ 클래스처럼 프로퍼티를 고정 오프셋으로 접근한다. 그래서 생성자에서 프로퍼티를 항상 같은 순서로 초기화하고, 이미 만든 객체에 나중에 프로퍼티를 추가/삭제(delete)하지 않는 것이 성능에 중요하다. 순서를 바꾸면 새 Map과 새 트랜지션 가지가 생겨 IC가 monomorphic(1개 shape)에서 polymorphic(여러 shape), 심하면 megamorphic(캐시 포기)로 전락하고, TurboFan은 deopt를 반복하게 된다. 또 하나 깊은 포인트는 'JIT의 투기성'과 GC의 트레이드오프다. TurboFan은 '이 함수 인자는 항상 SMI(작은 정수)' 같은 가정으로 코드를 만들다가 실수(float) 하나가 들어오면 deopt한다 — 그래서 hidden class와 타입을 안정적으로 유지하는 코드가 빠르다. GC 측면에서는 '대부분의 객체는 젊어서 죽는다(generational hypothesis)'는 가설 덕에 young 영역만 자주 값싸게 청소(Scavenge)하고, old는 드물게 비싸게 청소한다. 과거엔 GC가 메인 스레드를 멈춰(stop-the-world) 애니메이션이 끊겼지만, Orinoco 이후 마킹·스위핑·컴팩션 상당 부분을 백그라운드 스레드에서 동시(concurrent)·병렬(parallel)로 수행해 프레임 드랍을 크게 줄였다는 점을 설명하면 좋다.

쉽게 말하면 통역사가 처음엔 문장을 그때그때 통역하다, 자주 나오는 문장은 미리 외워(JIT) 즉답하는 것.

면접 예상 질문

#V8#JIT#Ignition#TurboFan#GC