JIT 컴파일링 — 학습 안내서
출처: Dev.to
배울 내용:
-
JVM이 무엇을 언제 컴파일할지 결정하는 방식
-
C1과 C2의 실제 차이점
-
Code Cache가 무엇이며 왜 성능을 떨어뜨릴 수 있는지
-
GraalVM: JIT vs Native Image
-
Project Leyden: Java 25가 워밍업 문제를 어떻게 해결했는지
목차
9. GraalVM vs 비 GraalVM — 실제 데이터 분석
-
섹션 1 — 원래 문제
왜 Java는 느렸고, 어떻게 해결했는가
모든 Java 개발자가 겪은 장면
💡 메서드 성능을 측정했을 때 처음에 200 ms가 나왔다.
30 초 뒤 다시 측정하면 8 ms가 나왔다.
무슨 일이 일어난 걸까? 애플리케이션이 “워밍업”되고 있었다.
이 현상에는 이름이 있고 메커니즘이 있으며, Java 25부터는 최종 해결책이 있다.
Java는 소스 코드(.java)를 바이트코드(.class)라는 중간 형식으로 컴파일한다. 이 바이트코드는 플랫폼에 독립적이며, JVM은 어느 머신이든 이 바이트코드를 해석한다. 바로 유명한 write once, run anywhere다.
하지만 해석은 명령어 하나하나를 실행하므로 느리다. 1996년에는 Java가 C보다 10~20배 느렸다.
접근 방식 비교
| 접근 방식 | 이식성 | 성능 | 예시 |
|---|---|---|---|
| 네이티브 컴파일 | ❌ OS별 바이너리 | ⭐⭐⭐⭐⭐ 최고 | C / C++ |
| 순수 인터프리터 | ✅ 범용 바이트코드 | ⭐ 낮음 | Java 1.0 (1996) |
| JIT — 하이브리드 | ✅ 범용 바이트코드 | ⭐⭐⭐⭐⭐ 높음 | Java 1.3+ (2000) |
| AOT Cache (Leyden) | ✅ 범용 바이트코드 | ⭐⭐⭐⭐⭐ 높음 + 빠른 시작 | Java 24+ (2024) |
💬 JIT을 떠올리게 만든 질문: “JVM이 실행 중에 학습해서 어떤 부분을 네이티브 코드로 컴파일해야 할지 스스로 판단할 수 있다면 어떨까? 이때 이식성은 포기하고 싶지 않다.”
섹션 2 — Just‑In‑Time Compilation
JVM이 실시간으로 관찰·결정·컴파일하는 과정
JIT은 HotSpot JVM이 실행 중인 코드를 관찰하고, 자주 호출되는 구간(hot spot)을 발견하면 별도 스레드에서 네이티브 머신 코드로 컴파일한다. 이 과정은 애플리케이션을 멈추게 하지 않는다.
JVM에서 메서드가 거치는 전체 과정
1단계 — 인터프리터 실행 (Tier 0)
JVM은 바이트코드를 한 줄씩 해석한다. 느리지만 즉시 실행 가능하다. 동시에 메서드당 두 개의 카운터가 증가한다:
-
Method invocation count — 메서드가 호출된 횟수
-
Loop back‑edge count — 메서드 내부 루프가 반복된 횟수
2단계 — C1에 의한 빠른 컴파일 및 프로파일링 (Tier 1–3)
카운터가 Tier3CompileThreshold(기본값: 2 000 호출)를 초과하면 메서드가 JIT 큐에 들어간다. C1 컴파일러가 빠르게 네이티브 코드를 생성하고 동시에 행동 데이터를 수집한다. 애플리케이션은 이미 빨라졌지만 아직 최적은 아니다.
3단계 — C2에 의한 공격적 최적화 (Tier 4)
메서드가 Tier4CompileThreshold(기본값: 15 000 호출)에 도달하면 C2가 작동한다. C2는 C1이 수집한 프로파일을 활용해 인라이닝, 디버추얼라이제이션, 이스케이프 분석 등 실시간 관찰 없이는 적용할 수 없는 최적화를 수행한다.
4단계 — Code Cache에 저장
C