JVM이 기동할 때마다 반복하던 준비 작업을 미리 해서 파일로 저장해 두는 기능(Project Leyden).
왜 기동이 느린가
- ① 클래스 파일 읽기 → ② 파싱·검증 → ③ 로드·링크
- ④ 인터프리터로 실행하며 ⑤ 프로파일 수집 → ⑥ JIT 컴파일
이 ①~⑥ 이 '매 기동마다' 반복된다 서버가 오래 사는 시절엔 상관없었지만, 컨테이너가 수시로 뜨고 죽으면 그대로 비용이다
무엇을 저장하나
JEP 483 (24) 로드·링크까지 끝난 클래스 → ①②③ 을 건너뛴다
JEP 514 (25) 캐시 생성을 한 단계로 축약
JEP 515 (25) 메서드 호출 프로파일 → ⑤ 를 건너뛰고 바로 ⑥
# 학습 실행 — 실제 기동과 같은 경로를 한 번 돌린다
java -XX:AOTCacheOutput=app.aot -jar app.jar
# 이후 기동은 캐시를 읽는다
java -XX:AOTCache=app.aot -jar app.jar
측정된 기동 시간 단축은 최대 42% 수준이다.
주의할 점
- 학습 실행 때 지나간 코드 경로만 캐시에 담긴다
- 기동만 하고 종료한 학습은 효과가 적다. 실제 트래픽 비슷하게 워밍업시킨다
- 한 단계 워크플로는 힙을 한 벌 더 쓴다 (-Xmx4g 면 약 8GB 필요)
- JDK 버전이나 클래스패스가 바뀌면 캐시가 무효가 된다
- CI 에서 만들어 이미지에 넣되 빌드 파이프라인에 묶어 둔다
GraalVM 네이티브 이미지와 무엇이 다른가
AOT 캐시 네이티브 이미지
결과물 평범한 JVM + 캐시 파일 독립 실행 바이너리
기동 수십 % 단축 밀리초 단위
제약 없음 — 코드 변경 0 닫힌 세계 가정
(런타임 리플렉션·동적 프록시를 미리 다 알아야)
힌트 설정 · 긴 빌드 시간
최고 성능 JIT 그대로 JIT 이 없어 장기 처리량은 손해일 수 있다
"네이티브 이미지는 부담스럽고 기동은 줄이고 싶다" 는 대부분의 서버가 AOT 캐시에 해당한다. 둘은 대체재가 아니라 비용이 다른 선택지다.
Java 26 — GC 제약이 풀렸다
25 까지는 캐시를 만들 때와 쓸 때의 GC 가 맞아야 했다. 그래서 지연 시간 때문에 ZGC 를 쓰는 서비스는 AOT 캐시를 못 쓰는 조합이 생겼다.
JEP 516 이 이 제약을 없앴다. 캐시에 담긴 객체를 특정 GC 의 힙 배치에 묶지 않도록 바꿔, ZGC 를 포함한 어떤 GC 와도 함께 쓸 수 있다. 낮은 꼬리 지연과 빠른 기동을 동시에 원하는 서비스가 이제 둘 다 가진다.
같은 세대의 다른 무기 — Compact Object Headers
java -XX:+UseCompactObjectHeaders -jar app.jar # Java 25 프로덕션 기능
객체 헤더를 두 워드(12B + 패딩)에서 한 워드(8B)로 줄인다 long 필드 하나짜리 객체: 24B → 16B (약 33% 절감) 작은 객체가 대량인 워크로드(컬렉션 노드 · 박싱된 숫자)에서 체감이 크다
- 힙 절약 + 캐시 지역성 개선
LTS 지형
- 21 LTS (2023-09) — 현재 주류. 가상 스레드가 여기서 정식화
- 25 LTS (2025-09) — 신규 기본값. Scoped Values 정식 · Compact Headers · AOT 캐시 완성
- 26 — (2026-03) non-LTS — 다음 LTS 의 예고편
- 17 LTS (2021-09) — 아직 많지만 감소 중
면접 함정
- ❌ "AOT 캐시를 쓰면 JIT이 없어진다" → JIT은 그대로다. 워밍업만 줄인다.
- ❌ "기동이 느리면 네이티브 이미지" → 먼저 AOT 캐시와 lazy-initialization을 본다. 훨씬 싸다.