Java HotSpot부터 Chrome V8까지, JIT의 등장 배경과 JavaScript 엔진의 최적화 파이프라인(Ignition & TurboFan)을 심층 분석합니다.
JIT 컴파일, 즉 프로그램을 실행하는 시점에 코드를 기계어로 번역하는 '동적 번역' 개념은 1960년대부터 존재했습니다. 하지만 당시에는 하드웨어 성능의 한계와 구현의 복잡성으로 인해 상용화되지 못하고 학술적인 개념에 머물러 있었습니다.
이 개념이 세상 밖으로 나오게 된 계기는 Java와 HotSpot의 등장 덕분입니다.
초기 Java는 'Write Once, Run Anywhere'를 목표로 운영체제에 독립적인 중간 언어인 바이트코드(Bytecode)를 사용했습니다. 하지만 초기 JVM은 이 바이트코드를 한 줄씩 해석하는 인터프리터 방식으로 동작하여 실행 속도가 매우 느리다는 치명적인 단점이 있었습니다.
이 문제를 해결하기 위해 썬 마이크로시스템즈는 1996년, 'HotSpot'이라는 이름의 JIT 컴파일러를 JVM에 도입했습니다.
이름 그대로 '뜨거운 지점(Hot Spot)', 즉 실행 시점에 자주 사용되는 코드를 찾아내어 네이티브 기계어로 컴파일하고 캐싱하는 기술입니다. 이로 인해 인터프리터의 느린 속도를 보완하면서도 전체를 미리 컴파일하는(AOT) 오버헤드를 줄여, Java가 엔터프라이즈 시장을 장악하는 발판이 되었습니다.
초기 JavaScript 역시 순수한 인터프리터 언어로, 복잡한 연산 성능이 매우 낮았습니다. 하지만 웹이 단순 문서 표시를 넘어 동적 애플리케이션 플랫폼으로 진화하면서 성능 향상이 절실해졌습니다.
JavaScript 엔진은 코드를 무작정 컴파일하지 않습니다. 인터프리터의 '빠른 시동'과 컴파일러의 '고속 주행' 장점만을 결합한 영리한 파이프라인을 가지고 있습니다.
V8 엔진의 핵심인 Ignition(인터프리터)와 TurboFan(최적화 컴파일러)의 협업 과정을 살펴봅시다.
엔진은 개발자가 작성한 JavaScript 코드를 읽어 들여 AST(Abstract Syntax Tree, 추상 구문 트리)라는 컴퓨터가 이해하기 쉬운 구조로 변환합니다. 이 과정에서 문법 오류 등을 검출합니다.
V8의 인터프리터인 Ignition은 AST를 기반으로 바이트코드를 생성하고 즉시 실행합니다.
수집된 프로파일링 데이터를 바탕으로 '최적화할 가치가 있는 코드(Hot Code)'가 식별되면, 이를 TurboFan으로 보냅니다.
최적화된 기계어가 생성되면 기존 바이트코드를 대체합니다. 하지만 만약 "숫자만 들어올 것"이라는 가정이 틀렸다면(예: 갑자기 문자열이 들어옴) 어떻게 될까요?
엔진은 즉시 최적화된 코드를 폐기(Deoptimization)하고, 다시 안전한 바이트코드(Ignition)로 돌아가 실행합니다. 그리고 프로파일링 데이터를 갱신하여 나중에 다시 최적화를 시도합니다.
이처럼 JIT는 "일단 빠르게 시작하고(Ignition), 자주 쓰는 건 최적화하고(TurboFan), 틀리면 원상복구(Deopt)한다"는 실용적인 전략을 취합니다.
엔진은 코드를 최적화하는 데 드는 비용(CPU, Memory) 대비 이익(속도 향상)을 끊임없이 계산합니다. 이 '눈치 게임'의 핵심 기준은 빈도(Frequency)입니다.
엔진 내부에는 각 함수나 코드 블록마다 카운터(Counter)가 존재합니다.
for,
while 루프가 많이 돌면 Hot Loop로 간주합니다.일정 횟수 이상 실행되어 Warm 상태가 되면 1차 최적화 후보가 되고, 임계값을 넘어 Hot 상태가 되면 TurboFan의 대상이 됩니다.
On-Stack Replacement (OSR) 기술을 사용합니다.
"카운팅 기반이라면, 사용자와 상호작용이 짧은 브라우저는 컴파일을 안 하고, 오래 켜져 있는 Node.js 서버는 다 컴파일하는 걸까요?"
그렇지 않습니다. 중요한 건 "무엇을, 얼마나 깊게" 컴파일하느냐입니다.
🖥️ 브라우저 (Chrome)
⚙️ 서버 (Node.js)