이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › JVM
JIT 워밍업
배포 직후의 첫 요청들은 왜 느린가
자바 코드는 바이트코드로 컴파일되어 JVM이 한 명령씩 해석하며 실행합니다. JVM은 자주 실행되는 메서드를 찾아내 기계어로 다시 컴파일하고(JIT), 그 뒤로는 훨씬 빨라집니다.
실행으로 대조 · JDK 21에서 실제로 잰 값
이 실습에 나오는 개념
- JIT 컴파일러
- 프로그램 실행 중에 자주 실행되는 바이트코드를 기계어로 컴파일해 두고 이후에는 그 기계어를 직접 실행하는 JVM의 구성 요소다. 처음에는 인터프리터로 바이트코드 명령을 하나씩 해석해 실행하다가, 반복 실행되는 부분을 찾아 최적화한다. 그래서 자바 애플리케이션은 기동 직후보다 어느 정도 실행된 뒤에 더 빨라진다(워밍업).
- 바이트코드
- 자바 컴파일러(javac)가 소스 코드를 변환해 만든 중간 형태의 코드로 .class 파일에 담긴다. 특정 CPU나 운영체제의 기계어가 아니라 JVM이 이해하는 명령어라서 어느 플랫폼의 JVM에서든 실행된다.
- JVM(자바 가상 머신)
- 자바 바이트코드를 읽어 실행하는 프로그램이다. 운영체제마다 그에 맞는 JVM이 있어서, 한 번 컴파일한 자바 프로그램을 수정 없이 여러 운영체제에서 실행할 수 있다. 클래스를 읽어 들이고, 메모리를 관리하고(가비지 컬렉션), 바이트코드를 기계어로 바꿔 실행하는 일을 맡는다.
- 클래스 로더
- 클래스 파일을 찾아 메모리로 읽어 들이는 JVM의 구성 요소다. 클래스는 프로그램 시작 때 전부 읽는 것이 아니라 처음 필요해지는 시점에 로딩된다. 읽어 들인 클래스는 JVM이 링킹(검증, 준비, 그리고 심볼릭 참조의 해석)과 초기화(static 초기화 실행)를 거쳐 사용할 수 있게 만든다.
이렇게 바꿔 보세요
- 처음 설정으로 — 첫 묶음은 5.8ms, 그다음부터는 1.4ms 안팎입니다.
- JIT를 끄면 — 끝까지 빨라지지 않습니다. 묶음마다 약 29ms.
- 미리 워밍업하면 — 사용자의 첫 요청부터 빠릅니다.
처음 설정으로 실행하면
- 자바 컴파일러(javac)는 소스를 기계어가 아니라 바이트코드로 바꿉니다. 프로그램이 시작되면 JVM의 인터프리터가 이 바이트코드를 한 명령씩 해석해 실행합니다. 어떤 CPU에서든 돌아가지만, 기계어를 직접 실행하는 것보다 훨씬 느립니다.
- 1번째 묶음: 5.8ms. 처음에는 인터프리터로 실행됩니다. JVM은 메서드가 몇 번 불렸는지, 반복문이 몇 번 돌았는지를 세고 있다가, 기준을 넘자 이 메서드를 기계어로 컴파일합니다. 이 묶음은 컴파일된 코드가 준비되기 전까지 해석 실행으로 돌았습니다(컴파일은 다른 스레드에서 진행).
- 2번째 묶음: 1.7ms. 컴파일된 기계어로 실행되기 시작했습니다. 첫 묶음보다 3~4배 빠릅니다. JVM은 실행하면서 모은 정보(어느 분기가 자주 타는지 등)로 한 번 더 강하게 최적화합니다.
- 3번째 묶음: 1.4ms.
- 4번째 묶음: 1.7ms.
- 5번째 묶음: 1.5ms.
- 6번째 묶음: 1.4ms.
- 7번째 묶음: 1.5ms.
- 8번째 묶음: 1.4ms.
- 9번째 묶음: 1.4ms.
- 10번째 묶음: 1.4ms.
- 11번째 묶음: 1.6ms.
- 12번째 묶음: 1.7ms.
실행 결과
- 같은 코드인데 처음과 나중의 속도가 다릅니다. JVM은 모든 코드를 미리 컴파일하지 않고, 실제로 자주 실행되는 부분만 골라 실행 중에 컴파일합니다(Just-In-Time). 실행 정보를 보고 최적화하므로 충분히 달궈진 뒤에는 미리 컴파일한 언어에 가까운 속도가 납니다.
- 배포 직후에는 JIT 컴파일 전인 데다 클래스 로딩, 커넥션 풀 채우기, 캐시가 비어 있는 것까지 겹쳐 첫 요청들이 유독 느립니다. 새 서버에 트래픽을 한꺼번에 넘기지 않고 조금씩 늘리는 이유이기도 합니다. 성능을 잴 때도 처음 구간은 버리고 충분히 달궈진 뒤의 값을 봅니다.
코드
static long work(int n) { // 측정한 메서드
long s = 0;
for (int i = 0; i < n; i++) s += (i * 31L) ^ (s >>> 3);
return s;
}
for (int batch = 1; batch <= 12; batch++) {
long t = System.nanoTime();
for (int k = 0; k < 2000; k++) work(1000); // 묶음 하나 = 2,000번 호출
print((System.nanoTime() - t) / 1000 + "마이크로초");
}
// 실행: java JitWarmup.java
이 모형의 전제
숫자는 이 PC의 JDK 21에서 위 코드를 실제로 실행해 잰 값입니다(기본 설정 두 번 실행 중 첫 번째, -Xint 한 번). "미리 워밍업"의 값은 기본 설정 측정값에서 첫 묶음을 빼고 이어 붙인 것이고, 마지막 값 하나(1.4ms)는 측정값이 아니라 비슷한 값을 덧붙인 것입니다. 수치는 CPU와 실행 시점에 따라 달라지지만, 첫 구간이 느리고 그 뒤로 빨라지는 모양은 같습니다. 기본 설정의 JVM은 가벼운 컴파일(C1)과 강한 최적화 컴파일(C2)을 단계적으로 적용합니다.
앞 실습: GC와 메모리 누수 · 다음 실습: 클래스 로딩과 초기화