JVM 예열은 Java 언어의 의미가 바뀌는 과정이 아니라, 같은 바이트코드 실행을 VM이 수집한 프로파일에 맞춰 다른 코드 경로로 구현하는 과정입니다. 따라서 시작 지연, 컴파일 비용, 최적화 코드의 steady 상태, 가정이 깨질 때의 deoptimization을 시간축으로 분리해 측정해야 합니다.
시작 직후 지연과 실행 환경 변화
Java 소스는 javac 같은 컴파일러가 class 파일의 바이트코드로 바꿉니다. JVM 명세는 이 코드의 의미를 정하지만 모든 구현이 항상 인터프리터만 쓰거나 모든 메서드를 즉시 JIT하도록 강제하지 않습니다. 관측 trace를 프로세스 시작 → 클래스 초기화 → 첫 호출 → 프로파일 수집 → 컴파일 완료 → steady 요청 → 새로운 타입 입력으로 놓으면, 각 지연 구간에 어떤 사건을 대조해야 하는지가 분명해집니다. 첫 호출만 느리고 이후 안정되면 초기화·예열 후보를, steady 구간에도 컴파일·GC가 겹치면 예열 정책과 자원 경쟁을 함께 의심합니다.
실제 VM은 해석 실행·실행 프로파일·네이티브 컴파일을 조합할 수 있습니다.
서버 시작에는 클래스 로딩·검증·링킹·초기화·연결 준비·캐시 채우기도 겹칩니다. 반복 요청이 빨라졌다고 모든 차이를 JIT 하나의 효과로 단정할 수 없습니다. 반대로 한 번의 짧은 실행만으로 오래 운영하는 서버의 정상 처리량을 대표할 수도 없습니다.
계층형 컴파일의 단계별 비용
HotSpot이 같은 메서드나 긴 루프를 반복해서 실행하면, 처음에는 해석 실행이나 비교적 빠른 초기 컴파일을 통해 실제 호출·타입·분기 정보를 프로파일로 모으고 이후 더 비싼 최적화를 적용할 수 있습니다. OSR(온스택 치환)은 이미 실행 중인 루프를 중간에 컴파일된 코드로 옮기는 방식이므로, 메서드가 반환된 뒤 다시 호출될 때만 바뀐다고 보지 않습니다. 단계·임계값·compiler 선택은 VM·JDK·옵션의 구현 계약입니다.
| 단계·현상 | 기대 이점 | 관측할 비용 |
|---|---|---|
| 클래스 준비·초기 실행 | 빠른 시작 가능 | 로딩·초기화·해석 비용 |
| 실행 정보 수집 | 실제 타입·분기 파악 | 프로파일·컴파일 CPU |
| 최적화 코드 사용 | 인라인·불필요 연산 제거 | 코드 캐시·가드 |
| 가정 변화 | 새 입력에 맞게 정확성 유지 | guard 실패·deopt·재컴파일 |
컴파일 스레드가 CPU를 쓰는 동안 요청 처리 스레드와 자원을 경쟁할 수 있습니다. 오토스케일로 여러 인스턴스를 동시에 띄우면 준비 비용이 겹칠 수 있어 readiness와 실제 용량 도달 시점을 구분합니다. 고정된 횟수의 예열만 넣기보다 실제 지연·처리량 곡선을 봅니다.
실행 프로파일과 언어 규칙의 분리
예를 들어 한 interface 호출 지점에서 실제 receiver가 거의 한 타입이면 VM은 그 타입인지 가드로 확인한 뒤 해당 구현을 인라인할 수 있습니다. 다른 타입이 들어와 가드가 실패하면 느린 경로를 타거나 deoptimization·재컴파일이 일어날 수 있지만, 한 번의 다른 타입 입력이 전체 메서드를 반드시 deopt한다는 뜻은 아니므로 컴파일 로그로 범위를 확인합니다.
그림을 글로 읽기
- 타입·분기 실행 정보 수집
- 가드가 있는 최적화 코드
- 다른 입력·가정 변화
- 대체 경로·deopt·재컴파일
- 타입·분기 실행 정보 수집 → 가드가 있는 최적화 코드: 가정 기반 최적화
- 가드가 있는 최적화 코드 → 다른 입력·가정 변화: 새 실행 분포
- 다른 입력·가정 변화 → 대체 경로·deopt·재컴파일: 정확성 유지
새 타입 입력 뒤 느린 요청이 생겼어도 클래스 로딩·GC·I/O·잠금이 원인일 수 있습니다. 요청 추적 시각과 JFR·컴파일 로그·CPU·GC를 대조해야 합니다. 인라인으로 소스의 함수 경계가 프로파일에서 다르게 보이는 것도 해석에 포함합니다.
Cold·Warmup·Steady의 분리 측정
짧은 CLI는 예열이 끝나기 전에 종료할 수 있어 시작 비용이 핵심입니다. 장수 서버는 안정 구간 처리량과 재배포·타입 분포 변화 때의 꼬리 지연이 중요합니다. 평균 하나로 두 목적을 합치지 않습니다.
시작 비용과 안정 구간 성능을 나눠 보려면 JDK 배포판·버전·VM 옵션·CPU quota·heap·입력·동시성·실행 프로세스 수를 고정합니다. JMH에서 warmup·measurement·fork를 분리하고 계산 결과를 실제로 소비해야 dead-code elimination과 상수 접기 때문에 벤치마크 코드가 사라지는 일을 피할 수 있습니다. 그래도 JMH는 네트워크·DB를 포함한 서비스 전체 측정이 아닙니다.
예열 입력이 한 타입·한 분기만 포함하면 운영의 다양한 입력에서 다른 가정과 지연이 나타날 수 있습니다. 대표 분포로 예열하되 희귀 경로·새 타입·cold 재시작도 별도 시험합니다. 코드 캐시·컴파일 큐·메모리 비용이 이득보다 큰지도 확인합니다.
시간축 근거 기반 가설 비교
새 프로세스 시작부터 요청 지연과 컴파일 이벤트를 기록하고 안정 구간 뒤 타입 다양성을 바꿉니다. 같은 시점의 deopt·클래스 로딩·GC·잠금·외부 I/O 중 무엇이 바뀌었는지 확인합니다. 재현 연습에서는 같은 fork 수와 입력 분포로 cold 요청, warmup 구간, steady 구간의 p50·p99와 CPU를 따로 기록하고, 한 타입만 예열한 뒤 다른 receiver를 섞습니다. 예상 결과는 새로운 타입이 들어온 시점에 guard/deopt가 보일 수 있지만, 그 지연이 반드시 deopt라는 뜻은 아니므로 GC·클래스 로딩·외부 I/O 이벤트가 같은 시점에 있는지 확인해야 한다는 것입니다.
Homebrew OpenJDK 21.0.12.1은 기능 예제 실행에 사용했지만 JFR·JMH·컴파일 로그 실험은 수행하지 않았습니다. 이 노트는 JVM 구현을 분석하는 학습 절차이며 특정 VM의 임계값이나 성능 향상 수치를 측정한 결과가 아닙니다.