실행 중에 자주 도는 코드를 기계어로 번역하는 컴파일러. 자바가 "느린 언어"가 아닌 이유다.
왜 처음엔 느린가
-
① 인터프리터가 바이트코드를 한 줄씩 해석하며 실행한다 (시작이 빠르다)
-
② 그러면서 '이 메서드가 몇 번 불렸나' 를 센다 (프로파일링)
-
③ 임계치를 넘으면 JIT 이 기계어로 번역한다 (hot spot)
-
④ 이후로는 번역된 코드가 실행된다 → 훨씬 빠르다
-
그래서 자바는 '워밍업' 후에 진짜 성능이 나온다
두 컴파일러
-
C1 (client) — 빨리 컴파일한다. 최적화는 가볍게 → 기동 초반
-
C2 (server) — 느리게 컴파일한다. 최적화가 공격적 → 오래 도는 코드
-
계층형 컴파일(Tiered) 이 기본 — 인터프리터 → C1 → C2 로 승격시킨다
무엇을 최적화하나
- 인라이닝 — 작은 메서드 호출을 본문으로 펼친다 (가장 효과가 크다)
- 루프 언롤링 — 반복 횟수를 줄여 분기 예측을 돕는다
- 탈출 분석 — 객체가 메서드를 벗어나지 않으면 힙 할당을 없앤다(스칼라 치환)
- 락 제거 경합이 없는 synchronized 를 지운다
- 분기 예측 활용 — 실제로 자주 가는 쪽을 앞에 둔다
인라이닝 덕분에 "getter를 쓰면 느리다"는 말이 성립하지 않는다 — 대부분 사라진다.
역최적화 (deoptimization)
JIT 은 '지금까지 관찰한 것' 을 전제로 최적화한다
- 예: 이 인터페이스의 구현체가 하나뿐이더라 → 가상 호출을 직접 호출로 바꾼다
나중에 두 번째 구현체가 로드되면 그 전제가 깨진다
- 최적화된 코드를 버리고 인터프리터로 되돌린다 (deopt)
- 다시 프로파일을 모아 재컴파일한다
그래서 성능이 '갑자기 나빠졌다 회복' 하는 구간이 생길 수 있다
벤치마크가 어려운 이유
// ❌ 이런 측정은 거의 무의미하다
long t = System.nanoTime();
for (int i = 0; i < 1000; i++) work();
System.out.println(System.nanoTime() - t);
// 워밍업 전 · 죽은 코드 제거 · 상수 접기로 측정 대상이 사라질 수 있다
JMH(Java Microbenchmark Harness)를 쓴다 — 워밍업 반복, 죽은 코드 제거 방지(Blackhole), 포크 등을 처리해 준다.
관찰하기
-XX:+PrintCompilation # 무엇이 컴파일됐나
-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
AOT와의 관계
- JIT — 실행하며 번역. 실제 프로파일을 아니까 최적화가 좋다. 대신 워밍업이 필요하다
- AOT — 미리 번역 (GraalVM 네이티브 이미지). 즉시 빠르지만 최고 성능은 JIT 이 낫다
- AOT 캐시 (Leyden) 로드·링크 결과와 프로파일을 저장해 워밍업만 줄인다. JIT 은 그대로
면접 함정
- ❌ "자바는 인터프리터 언어라 느리다" → 워밍업 후에는 네이티브에 근접한다.
- ❌ "JIT은 모든 코드를 컴파일한다" → 자주 도는 코드만 한다. 한 번 도는 코드는 인터프리터가 낫다.
워밍업을 실제로 관찰하기
for (int round = 0; round < 5; round++) {
long t = System.nanoTime();
for (int i = 0; i < 1_000_000; i++) work();
System.out.println(round + ": " + (System.nanoTime()-t)/1_000_000 + "ms");
}
// 0: 180ms ← 인터프리터
// 1: 45ms ← C1
// 2: 12ms ← C2
// 3: 11ms
// 4: 11ms ← 안정화
초반 몇 라운드를 버리지 않으면 벤치마크가 무의미한 이유가 이 표에 다 있다.
컴파일 임계치
- -XX:TieredStopAtLevel=1 — C1 까지만 (기동은 빠르고 최고 성능은 포기 — 짧게 도는 배치·CLI 에 유용)
- -XX:CompileThreshold=10000 — 컴파일 임계치 (계층형에서는 다른 옵션이 쓰인다)
- -XX:+PrintCompilation — 무엇이 언제 컴파일됐나
- -XX:ReservedCodeCacheSize — 컴파일된 코드가 담기는 영역. 꽉 차면 컴파일이 중단된다
CodeCache가 가득 차면 CodeCache is full. Compiler has been disabled 경고와 함께 성능이 급락한다 — 큰 애플리케이션에서 실제로 겪는 문제다.
프로파일 오염
공통 유틸 메서드가 여러 타입으로 불리면
- JIT 이 "이 호출 지점은 다형적이다" 로 판단해 인라이닝을 포기한다
- 특정 경로만 느려지는 원인 모를 성능 저하가 된다