자바 언어·플랫폼 용어 사전
GC·JITJust-In-Time · C1 · C2 · 역최적화

JIT

자주 실행되는 코드를 기계어로 번역하는 컴파일러. 그래서 자바는 워밍업 후에 빨라진다.

실행 중에 자주 도는 코드를 기계어로 번역하는 컴파일러. 자바가 "느린 언어"가 아닌 이유다.

왜 처음엔 느린가

  • ① 인터프리터가 바이트코드를 한 줄씩 해석하며 실행한다 (시작이 빠르다)

  • ② 그러면서 '이 메서드가 몇 번 불렸나' 를 센다 (프로파일링)

  • ③ 임계치를 넘으면 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 이 "이 호출 지점은 다형적이다" 로 판단해 인라이닝을 포기한다
  • 특정 경로만 느려지는 원인 모를 성능 저하가 된다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — GC·JIT는 무대 뒤에서 — 내부와 실무 튜닝