- Published on
JIT 컴파일 완전 가이드 2025: V8, JVM HotSpot, .NET, Tiered Compilation, 성능 분석
- Authors

- Name
- Youngju Kim
- @fjvbn20031
TL;DR
- JIT = 인터프리터의 속도 + 컴파일러의 유연성
- Tiered Compilation: 빠른 시작 + 점진적 최적화
- V8 (Chrome, Node.js): Ignition (인터프리터) → Maglev → TurboFan
- JVM HotSpot: 인터프리터 → C1 → C2 (또는 Graal)
- .NET: ReadyToRun → Tier 0 → Tier 1
- Inline Caching, Speculation, Deoptimization: JIT의 핵심 기법
1. 컴파일 모델
1.1 3가지 접근
1. 인터프리터:
- 코드를 직접 실행
- 빠른 시작
- 느린 실행
2. AOT (Ahead-Of-Time):
- 미리 기계어로 컴파일
- 빠른 실행
- 느린 시작 (컴파일 시간)
- 동적 최적화 X
3. JIT (Just-In-Time):
- 런타임에 컴파일
- 둘의 장점 결합
1.2 비교
| 인터프리터 | AOT | JIT | |
|---|---|---|---|
| 시작 | 빠름 | 느림 | 빠름 |
| 실행 | 느림 | 빠름 | 빠름 (warm-up 후) |
| 최적화 | 없음 | 정적 | 동적 |
| 메모리 | 적음 | 적음 | 많음 |
| 예 | Python (CPython) | C, Rust | Java, JS, C# |
1.3 JIT의 장점
1. 프로파일 기반 최적화:
- 어떤 코드가 자주 실행되는지 측정
- "hot" 코드만 최적화
2. 가정 기반 최적화:
- "이 변수는 항상 정수다"라고 가정
- 틀리면 deoptimize
3. 인라이닝:
- 함수 호출을 본문으로 대체
- 함수 호출 오버헤드 제거
4. 동적 정보 활용:
- 런타임 타입 정보
- 분기 예측 데이터
2. JIT가 작동하는 방식
2.1 기본 흐름
[Source Code]
↓ Parse
[AST]
↓ Compile to Bytecode
[Bytecode]
↓ Execute (Interpreter)
[Profile (어떤 함수가 hot?)]
↓ JIT Compile (hot 함수만)
[Native Code]
↓ Execute (Fast)
[Deoptimize if assumption wrong]
↓
[Bytecode (다시)]
2.2 Hot Spot 감지
Method-level:
- 함수 호출 횟수 카운트
- 임계값 (예: 10,000) 넘으면 컴파일
Loop-level:
- 루프 반복 횟수 카운트
- "OSR (On-Stack Replacement)"
2.3 OSR — On-Stack Replacement
void process() {
for (int i = 0; i < 1_000_000_000; i++) {
// 매우 hot
}
}
문제: 함수가 한 번만 호출되어도 루프가 hot.
해결: OSR — 실행 중인 인터프리터를 컴파일된 코드로 교체.
[Interpreter at iteration 100,000]
↓ JIT compile
[Native code]
↓ OSR: 인터프리터 → 네이티브
[Iteration 100,001 in native code]
2.4 컴파일 임계값
| 시스템 | Method 임계값 | OSR 임계값 |
|---|---|---|
| HotSpot C1 | 1,500 | 13,500 |
| HotSpot C2 | 10,000 | 14,000 |
| V8 Maglev | ~1,000 | |
| V8 TurboFan | ~10,000 |
너무 낮음: 컴파일 오버헤드. 너무 높음: 인터프리터에서 너무 오래.
3. V8 (Chrome, Node.js)
3.1 V8의 컴파일 파이프라인
[JavaScript]
↓ Parse
[AST]
↓ Bytecode
[Ignition (Interpreter)]
↓ Profile
[Sparkplug (baseline JIT)]
↓ Hot
[Maglev (mid-tier)]
↓ Hotter
[TurboFan (top-tier optimizer)]
3.2 Ignition — Bytecode Interpreter
2016 도입.
역할:
- AST를 bytecode로 컴파일
- 빠른 시작 (메모리 효율)
- 인라인 캐싱
왜 인터프리터?:
- 모바일에서 메모리 절약
- 시작 시간 단축
3.3 Sparkplug — Baseline JIT
2021 도입.
역할:
- Bytecode를 즉시 기계어로 (최적화 X)
- Ignition보다 5-15% 빠름
- 매우 빠른 컴파일 (인터프리터 직접 변환)
3.4 Maglev — Mid-tier Optimizer
2023 도입.
역할:
- TurboFan 전 단계
- Sparkplug보다 빠름
- TurboFan보다 컴파일 빠름
- Tier 사이 균형
3.5 TurboFan — Top-tier Optimizer
2017 도입 (Crankshaft 대체).
역할:
- 가장 최적화된 코드 생성
- Sea-of-Nodes IR
- 인라이닝, 분기 예측, escape analysis
- SIMD 활용
컴파일 비용: 매우 큼 (~ms 단위), hot 함수만.
3.6 Inline Caching
JavaScript의 동적 디스패치 최적화.
function getX(obj) {
return obj.x // obj의 형태를 모름
}
// Hidden Class 학습
getX({x: 1, y: 2}) // Class A
getX({x: 3, y: 4}) // Class A (재사용)
getX({x: 5, y: 6, z: 7}) // Class B (다른 형태)
V8이 학습:
- 첫 호출: monomorphic (Class A만)
- 두 번째: 여전히 monomorphic
- 세 번째: polymorphic (Class A, B)
- 5+: megamorphic (느림)
Monomorphic이 가장 빠름. 같은 형태 객체를 일관되게 사용.
3.7 Hidden Classes
JavaScript는 동적 타입이지만, V8 내부에서는 숨은 클래스를 사용:
const obj1 = {} // Class C0
obj1.x = 1 // Class C1 (x 필드 추가)
obj1.y = 2 // Class C2
const obj2 = {} // Class C0
obj2.x = 1 // Class C1 (재사용!)
obj2.y = 2 // Class C2 (재사용!)
같은 순서로 필드 추가 → 같은 hidden class → 빠름.
다른 순서면:
const obj3 = {}
obj3.y = 1 // Class D1 (다른 경로)
obj3.x = 2 // Class D2
// obj1, obj2와 다른 클래스 → 느림
팁: 객체 생성 시 모든 필드를 한 번에:
function makePoint(x, y) {
return { x, y } // 모든 필드 같은 순서
}
4. JVM HotSpot
4.1 컴파일 파이프라인
[Java source]
↓ javac
[Bytecode (.class)]
↓ JVM
[Interpreter]
↓ Hot (1,500 호출)
[C1 (Client Compiler)]
↓ Very Hot (10,000 호출)
[C2 (Server Compiler)]
↓ 또는
[Graal]
4.2 C1 (Client Compiler)
역할:
- 빠른 컴파일
- 보통의 최적화
- 빠른 시작에 적합
사용: 짧은 프로그램, 데스크톱 앱.
4.3 C2 (Server Compiler)
역할:
- 매우 공격적 최적화
- 느린 컴파일
- 긴 실행 프로그램에 적합
최적화:
- 인라이닝
- 루프 풀기 (loop unrolling)
- Escape analysis
- Branch prediction
- Dead code elimination
- Constant folding
4.4 Tiered Compilation (JDK 8+)
기본: 인터프리터 → C1 → C2.
장점:
- 빠른 시작 (인터프리터 + C1)
- 최종적으로 최고 성능 (C2)
# 활성화 (기본)
java -XX:+TieredCompilation MyApp
# C2만 (고성능)
java -XX:-TieredCompilation -server MyApp
4.5 Graal — 새로운 컴파일러
Oracle이 만든 차세대 JIT. C2 대체.
장점:
- Java로 작성 (C2는 C++)
- 더 나은 최적화 (특정 워크로드)
- 다른 언어 지원 (GraalVM)
java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler MyApp
4.6 Escape Analysis
public Point getPoint() {
Point p = new Point(1, 2); // 객체 생성
return p.x + p.y; // 객체가 함수 밖으로 안 나감
}
C2가 분석:
Point객체가 함수 밖으로 안 나감- → Heap에 할당 X, 스택에 할당 (매우 빠름)
- 또는 scalar replacement (필드만 사용)
효과: GC 부담 감소, 캐시 친화적.
4.7 Inlining
int square(int x) { return x * x; }
int sumSquares(int n) {
int sum = 0;
for (int i = 0; i < n; i++)
sum += square(i);
return sum;
}
C2가 인라이닝:
int sumSquares(int n) {
int sum = 0;
for (int i = 0; i < n; i++)
sum += i * i; // square 인라이닝
return sum;
}
효과:
- 함수 호출 오버헤드 제거
- 추가 최적화 가능 (loop fusion 등)
한계: 너무 큰 함수는 인라이닝 X (코드 크기 폭증 방지).
5. .NET CLR
5.1 컴파일 파이프라인
[C# source]
↓ Roslyn
[CIL (Common Intermediate Language)]
↓ CLR
[ReadyToRun (옵션, AOT)]
↓
[Tier 0 (빠른 컴파일)]
↓ Hot
[Tier 1 (최적화)]
5.2 ReadyToRun (R2R)
.NET Core 2.1+ AOT 컴파일.
역할:
- 시작 시간 단축
- IL을 미리 네이티브로
- 런타임에 추가 JIT 가능
dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true
효과:
- 시작 시간 50%+ 단축
- 메모리 절감
5.3 Tiered Compilation
.NET Core 3.0+ 기본.
Tier 0: 빠른 컴파일, 최적화 적음. Tier 1: 느린 컴파일, 공격적 최적화.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
int Square(int x) => x * x;
5.4 RyuJIT
.NET Core의 기본 JIT.
개선 (.NET 8+):
- ARM64 지원
- 동적 PGO (Profile-Guided Optimization)
- 더 빠른 컴파일
5.5 NativeAOT
.NET 7+: 완전 AOT.
dotnet publish -p:PublishAot=true
장점:
- 매우 빠른 시작
- 작은 바이너리
- 컨테이너에 적합
- JIT 없음 → 메모리 적음
단점:
- 런타임 동적 최적화 X
- Reflection 제한
- 일부 라이브러리 호환 X
6. PGO — Profile-Guided Optimization
6.1 정적 PGO
- 빌드 1: 계측 코드 추가
- 실행: 프로파일 수집
- 빌드 2: 프로파일 사용해 최적화
# Clang
clang -fprofile-generate -o app app.c
./app # profile.profdata 생성
clang -fprofile-use=profile.profdata -O2 -o app app.c
효과:
- 5-30% 성능 향상
- 분기 예측 개선
6.2 동적 PGO (.NET, JVM)
JIT가 런타임에 자동으로:
- 어떤 분기가 자주 taken인가?
- 어떤 함수가 hot인가?
- 어떤 객체 형태가 자주 보이나?
→ 다음 컴파일에 활용.
장점: 사용자 패턴에 자동 적응.
7. Tracing JIT
7.1 Method JIT vs Tracing JIT
Method JIT (HotSpot, V8):
- 함수 단위로 컴파일
- Java, JavaScript
Tracing JIT (PyPy, LuaJIT):
- 실행 경로(trace) 단위로 컴파일
- "이 루프의 이 분기"
- 매우 공격적 최적화
7.2 PyPy
Python 인터프리터 + tracing JIT.
장점:
- CPython보다 5-10배 빠름
- 100% 호환
단점:
- 시작 느림
- 메모리 많이 사용
- C 확장 호환 문제
7.3 LuaJIT
Lua의 tracing JIT.
Mike Pall의 작품. 종종 C와 비슷한 성능.
사용:
- WoW (Lua scripts)
- nginx (Lua module)
- OpenResty
7.4 Tracing의 어려움
- Trace 폭증: 가능한 경로가 너무 많으면
- Trace 무효화: 가정이 깨지면
- 메모리: 많은 trace 저장
→ Method JIT가 더 일반적.
8. Speculation과 Deoptimization
8.1 Speculative Optimization
JIT는 가정을 합니다:
예시: V8
function add(a, b) {
return a + b
}
// 1000번 호출, 항상 정수
add(1, 2)
add(3, 4)
// V8: "a, b는 항상 정수다" → 정수 덧셈으로 최적화
컴파일된 코드:
; 정수 덧셈 (매우 빠름)
add eax, ebx
가정 검증: 매번 타입 체크.
8.2 가정이 깨지면?
add("hello", "world") // 문자열!
문제: 정수 덧셈 코드는 문자열 처리 못 함.
해결: Deoptimization.
[Native Code (정수 덧셈)]
↓ Type check 실패
[Deopt to Bytecode]
↓ Re-execute in interpreter
[String concatenation]
비용: deopt는 비쌈. 자주 발생하면 JIT가 포기.
8.3 Deopt 방지
일관된 타입 사용:
// 좋음 - 항상 정수
function add(a, b) {
return a + b
}
add(1, 2)
add(3, 4)
// 나쁨 - 타입 혼용
function process(x) {
if (typeof x === 'string') return x.toUpperCase()
return x * 2
}
8.4 Megamorphic 함수
function getName(obj) {
return obj.name
}
getName({name: "Alice"})
getName({name: "Bob", age: 30})
getName({name: "Charlie", age: 25, country: "KR"})
// 5+ 다른 hidden class → megamorphic
Megamorphic = 느림. V8의 inline cache가 polymorphic까지만 효율적.
9. 디버깅과 분석
9.1 V8
Node.js:
node --trace-opt --trace-deopt my-app.js
출력:
[marking 0x... for optimized recompilation]
[deoptimizing 0x... because of wrong map]
Chrome DevTools:
- Performance 탭
- "Bottom-Up" 분석
- Optimized vs not optimized
9.2 JVM
JIT 컴파일 로그:
java -XX:+PrintCompilation MyApp
JITWatch: GUI 도구.
Async-profiler: 샘플링 프로파일러.
./profiler.sh -d 30 -f profile.html <pid>
9.3 .NET
dotnet-trace:
dotnet-trace collect --process-id <pid>
PerfView: Windows 도구.
BenchmarkDotNet: 마이크로벤치마크.
[Benchmark]
public int Sum() {
int sum = 0;
for (int i = 0; i < 1000; i++) sum += i;
return sum;
}
10. 실전 — 빠른 코드 작성
10.1 V8 (JavaScript)
Do:
- ✅ 일관된 객체 형태 (모든 필드 같은 순서)
- ✅ 일관된 타입 (정수만, 문자열만)
- ✅ Hot 함수를 작게 (인라이닝 가능)
- ✅ Monomorphic 호출 사이트
Don't:
- ❌ 객체에 동적으로 필드 추가
- ❌
delete키워드 (hidden class 무효화) - ❌ try/catch in hot paths (V8은 try 안의 코드 최적화 X — 옛 버전)
- ❌
arguments객체 (rest parameters 사용)
10.2 JVM
Do:
- ✅ 작은 메서드 (인라이닝)
- ✅ Final 필드/클래스 (devirtualization)
- ✅ Primitive 타입 우선
- ✅ Escape analysis 활용 (지역 객체)
Don't:
- ❌ 거대 메서드 (인라이닝 안 됨)
- ❌ 과도한 reflection (deoptimize)
- ❌ JIT warm-up 무시 (벤치마크에 중요)
10.3 .NET
Do:
- ✅
[MethodImpl(MethodImplOptions.AggressiveInlining)] - ✅
Span<T>사용 (heap 회피) - ✅ struct 활용 (escape analysis)
- ✅ NativeAOT for short-lived
Don't:
- ❌ Boxing (object로 변환)
- ❌ LINQ in hot paths
- ❌ Reflection in hot paths
10.4 일반 원칙
- 측정 먼저 — 추측하지 말기
- 핫 루프에 집중 — Pareto 원칙
- JIT warm-up 고려 — 첫 1초는 느림
- 벤치마크는 microbench가 아닌 실제 부하
퀴즈
1. JIT가 인터프리터와 AOT의 장점을 어떻게 결합하나요?
답: 인터프리터의 빠른 시작 + 컴파일러의 빠른 실행. 처음에는 인터프리터로 빠르게 시작 → 자주 실행되는 "hot" 코드를 프로파일링 → JIT가 그 부분만 기계어로 컴파일. AOT보다 우월한 점: (1) 런타임 정보 활용 (실제 사용 패턴), (2) Speculative optimization (가정 기반), (3) Deoptimization (가정 틀리면 인터프리터로 돌아감). 단점: 메모리 사용량 증가, warm-up 필요.
2. V8의 Hidden Class가 왜 중요한가요?
답: JavaScript는 동적 타입이지만 V8 내부는 숨은 클래스로 객체 형태를 추적합니다. 같은 순서로 필드 추가 → 같은 hidden class → V8이 효율적으로 코드 생성. 다른 순서 → 다른 hidden class → 느림. 팁: 객체 생성 시 모든 필드를 한 번에 ({ x, y }), delete 사용 X, 일관된 형태 유지. Monomorphic > Polymorphic > Megamorphic — 같은 형태로 호출하면 100배+ 빠를 수 있음.
3. JVM HotSpot의 C1과 C2의 차이는?
답: C1 (Client Compiler): 빠른 컴파일, 보통의 최적화. 짧은 프로그램이나 데스크톱 앱에 적합. C2 (Server Compiler): 느린 컴파일, 공격적 최적화 (인라이닝, escape analysis, loop unrolling). 긴 실행 프로그램에 적합. Tiered Compilation (JDK 8+ 기본): 인터프리터 → C1 → C2 — 빠른 시작 + 최종 고성능. Graal은 C2를 대체할 차세대 컴파일러 (Java로 작성).
4. Speculative Optimization과 Deoptimization은?
답: Speculation: JIT가 가정을 함 (예: "a, b는 항상 정수"). 가정 기반으로 매우 빠른 코드 생성. Deoptimization: 가정이 깨지면 인터프리터로 돌아감. 예: add(1, 2) 1000번 호출 → 정수 덧셈으로 최적화 → add("hello", "world") 호출 → 가정 깨짐 → deopt → 인터프리터에서 문자열 처리. deopt는 비쌈. 자주 발생하면 JIT가 그 함수를 포기. 일관된 타입 사용이 핵심.
5. NativeAOT는 언제 사용하나요?
답: NativeAOT 적합: (1) 컨테이너/Lambda — 빠른 시작 시간 필수, (2) CLI 도구 — 짧은 실행, (3) 임베디드 — 작은 바이너리, (4) JIT 메모리 부담 회피. 장점: 매우 빠른 시작, 작은 바이너리, JIT 없어 메모리 적음. 단점: 런타임 동적 최적화 없음 (긴 실행 프로그램은 JIT가 더 빠를 수 있음), Reflection 제한, 일부 라이브러리 호환 안 됨. .NET 7+, GraalVM Native Image가 대표적. 장기 실행 서버는 JIT, 단기 실행은 AOT.