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로 회수한다.
내부 구성
핵심 포인트
- 실행 파이프라인: 소스 → 스캐너(토큰) → 파서(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) 즉답하는 것.