Java2026년 7월 29일3분 읽기

JIT 워밍업: 배포 직후 첫 요청이 느린 이유

#Java#JVM#JIT#워밍업#콜드스타트#성능#CS 기초

JIT 워밍업: 배포 직후 첫 요청이 느린 이유

JIT 컴파일과 워밍업을 공부하면서 정리한 내용이다. 배포 직후 첫 요청이 유독 느리다는 얘기를 종종 듣는데, 코드가 바뀐 것도 아닌데 같은 엔드포인트가 처음엔 느리고 나중엔 빨라진다고 한다. 원인은 트래픽이 아니라 JVM이 코드를 실행하는 방식에 있다. 왜 그런지 따라가 봤다.

자바는 처음엔 한 줄씩 통역한다

자바는 부팅 직후 바이트코드를 인터프리터로 실행한다. 한 줄씩 읽어서 기계어로 통역하며 돌리는 방식이라 느리다. 대신 JVM은 실행되는 메서드의 호출 횟수를 세다가, 자주 불리는(hot) 메서드를 JIT 컴파일러가 통째로 기계어로 컴파일해 캐시(코드 캐시)에 넣는다. 그다음부터 그 메서드는 통역 없이 컴파일된 기계어로 바로 돈다.

컴파일은 두 단계다. C1이 약 2,000회 호출에서 가볍게 컴파일하고, C2가 약 15,000회에서 인라이닝 같은 더 공격적인 최적화까지 걸어 컴파일한다. 그래서 같은 메서드라도 처음 몇천 번은 통역으로, 그 뒤로는 컴파일된 코드로 실행된다. 부팅 직후는 코드 캐시가 텅 비어 있으니 전부 통역이다. 배포 직후가 느린 게 여기서 나온다.

예제로 확인

개념만 보면 그렇다는 거고, 실제로 그런지 간단한 예제로 확인해봤다. 계산이 좀 있는 메서드 하나(hot)를 5만 번 반복 호출하면서, 매 호출의 지연을 재서 구간별 평균을 냈다. 코드가 안 바뀌니 지연이 일정해야 정상인데, 실제로는 이랬다.

text
호출 구간           평균 지연(µs)
     0 ~ 100        4.922
   100 ~ 1000       0.685
  1000 ~ 2000       0.622
  2000 ~ 5000       0.526
  5000 ~ 15000      0.516
 15000 ~ 50000      0.526

첫 50호출 평균이 5.3µs, 워밍업이 끝난 뒤(4만5천~5만 구간)가 0.51µs. 같은 코드인데 10배 넘게 빨라졌다. 대부분의 낙차는 초반 1,000호출 안에서 떨어진다. 실행이 반복될수록 JIT가 이 메서드를 컴파일해 캐시에 넣었다는 뜻이다.

정말 그때 컴파일된 건지는 -XX:+PrintCompilation으로 볼 수 있다. hot 메서드에 대한 로그만 추리면 이렇게 나온다.

text
12    8       3       JitWarmup::hot (47 bytes)              ← level 3 (C1)
12    9 %     4       JitWarmup::hot @ 5 (47 bytes)          ← level 4 (C2), % = OSR
13   10       4       JitWarmup::hot (47 bytes)

시작 12~13ms 사이에 C1(level 3)으로 컴파일됐다가 곧바로 C2(level 4)로 다시 컴파일됐다. 지연 커브가 뚝 떨어지는 지점과 컴파일이 일어난 시점이 맞아떨어진다. 추측이 아니라 로그로 확인되는 부분이다.

JIT를 끄면 어떻게 되나

코드 캐시가 영원히 안 채워지는 상황, 즉 -Xint로 JIT를 꺼 순수 인터프리터로만 돌리면 배포 직후 워밍업 전 상태가 계속 유지되는 셈이다.

text
기본 (JIT 켬)         전체 50,000회  →   27 ms
-Xint (인터프리터만)   전체 50,000회  →  290 ms

10배가 넘게 차이 난다. -Xint는 워밍업 커브도 없이 처음부터 끝까지 5~8µs로 평평하다. 배포 직후 코드 캐시가 비어 있을 때가 딱 이 상태에 가깝고, 여기에 트래픽까지 한꺼번에 몰리면 모든 요청이 인터프리터로 처리되면서 응답이 밀리거나 심하면 서버가 넘어간다. 참고로 C1까지만 켜도(-XX:TieredStopAtLevel=1) 31ms라, 이 워크로드에선 C1만으로도 대부분의 이득을 봤다.

워밍업으로 해결한다

해법은 실제 트래픽을 받기 전에 주요 코드 경로를 미리 실행해서 코드 캐시를 채워두는 것이다. 서버가 뜨면 자주 쓰는 API를 자체적으로 몇천 번 호출해 hot 메서드들을 컴파일시켜 놓는다. 쿠버네티스라면 readiness probe로, 이 워밍업이 끝나기 전까지 로드밸런서가 트래픽을 안 보내게 막는다. 준비가 안 된 인스턴스에 요청이 들어가는 걸 원천 차단하는 방식이다.

정리

자바는 부팅 직후엔 바이트코드를 인터프리터로 실행하다가, 호출이 쌓이면 JIT가 hot 메서드를 컴파일해 캐시에 넣으면서 같은 코드가 점점 빨라진다. 배포 직후가 느린 건 코드 캐시가 비어 전부 인터프리터로 도는 구간이기 때문이고, 그래서 트래픽을 받기 전에 워밍업으로 미리 데워두는 것이다.

#Java#JVM#JIT#워밍업#콜드스타트#성능#CS 기초

황호민

Backend Engineer · Java/Kotlin · Spring Boot · Next.js