HotSpot의 반격: JEP 544, GraalVM 시작 성능 독점의 끝인가?
HotSpot JVM에 흥미진진한 시대가 찾아왔습니다: JEP 544: Ahead-of-Time Code Compilation이 Java 28용으로 방금 제안되었습니다. 이를 통해 HotSpot AOT 캐시가 GraalVM에 필적하는 시작 성능을 확보하되, GraalVM의 그런 고충은 하나도 겪지 않아도 될 것으로 기대됩니다. 다만 AOT 캐시를 학습시키는 것은 GraalVM으로 사전 컴파일하는 것과는 다르기 때문에, 실제로는 어느 쪽이 어떻게 비교될까요?AOT 캐시, 그 배경 이야기
Java는 20년 넘게 시작 속도 개선을 시도해 왔습니다. Java 5의 클래스 데이터 공유(CDS)로 시작해, Java 10에서는 AppCDS로 애플리케이션 클래스를 포함시키고, Java 13에서는 학습 프로세스를 간소화했습니다. 하지만 이 가운데 실제로 수치나 채택 면에서 정말 인상적인 영향을 낸 것은 없었습니다.
2022년 Mark Reinhold는 Java 시작을 대폭 개선하는 것을 목표로 하는 “Project Leyden”을 발표했습니다. 시간이 흘러 Java 24에는 JEP 483, 즉 HotSpot AOT 캐시가 포함되었습니다. AOT 캐시는 CDS의 아이디어를 기반으로 하면서도, 학습 실행(training run) 이후의 클래스 링크 상태와 아카이브된 힙 객체까지 저장하는 더 일반적인 메커니즘을 추가합니다.
Java 25는 JEP 514로 AOT 캐시를 실제로 사용할 수 있게 했고, JEP 515로 메서드 프로파일링 결과 캐싱을 추가했습니다. 이제 컴파일된 메서드를 캐싱하는 제안된 JEP 544와 함께, Project Leyden은 Java 시작을 위해 약속했던 킬러급 개선 사항을 전달하는 마지막 단계에 접어들었습니다.
얼마나 더 빨라질까?
대부분의 성능 개선이 그렇듯이, 실제 수치는 애플리케이션이 실제로 하는 일에 따라 크게 달라질 수 있습니다. 좋은 실제 사례가 install4j IDE입니다. 이 IDE는 메인 창을 표시하려면 시작 시 1만 개의 클래스를 로드해야 합니다. 제 MacBook에서는 예전에 약 3초가 걸렸습니다. Java 25의 AOT 캐시를 사용하면 1.5초 만에 뜹니다. 두 배의 속도는 놀라운 성과이며 사용자 경험에도 큰 차이를 만듭니다. 느낌이 “느릿느릿”하던 데서 “빠릿빠릿”으로 완전히 바뀌는 것이죠.
Java 28에서는 단지 JDK를 업그레이드하는 것만으로 또 한 번의 이런 개선이 약속되어 있습니다. 복잡한 IDE의 1초 미만 시작 시간은 놀라울 정도로 빠르며, Java 시작이 느리다는 낡은 선입견은 이제 영원히 은퇴시켜야 할 때입니다. 다음은 JEP에 실린 성능 그래프입니다:
이는 실로 놀라운 수준이며, 이전 상황이 얼마나 좋지 않았는지를 잘 보여줍니다.
그렇다면 GraalVM은?
폴리글랏 VM이자 C2 HotSpot 컴파일러 대체제라는 뿌리에서 출발한 GraalVM은 이제 JVM 바이트코드용 네이티브 이미지 생성기로 변모했습니다. Quarkus, Helidon, Spring Boot를 비롯해 생태계에서 널리 채택되고 있습니다.
GraalVM을 사용하면 JVM 없이 단일 바이너리를 얻을 수 있고, 시작과 워크로드 모두에서 훌륭한 성능을 얻을 수 있습니다.
이토록 훌륭하다면 왜 모두가 사용하지 않을까요? 많은 좋은 것들이 그렇듯이 함정이 있는데, 바로 클로즈드 월드(closed world) 요구 사항입니다. 프로그램이 할 수 있는 모든 것은 빌드 시점에 알려져 있어야 합니다. 동적 동작은 사전에 선언해야 합니다: 리플렉션, 동적 프록시, 리소스, 직렬화 등 목록은 계속됩니다. 게다가 Swing 앱은 GraalVM 네이티브 이미지에서 전혀 지원되지 않습니다(install4j IDE가 그 예입니다).
두 기술 비교하기
비교를 위해 저는 의도적으로 GraalVM에 유리한 앱을 골랐습니다: BetterBranch는 현재 브랜치를 기준으로 모든 로컬 Git 브랜치의 ahead/behind 상태를 알려주는 깔끔한 명령줄 도구이며, GraalVM 바이너리로 배포됩니다.
아래는 Linux에서 hyperfine으로 측정한 수치입니다. “실제 워크로드” 열은 BetterBranch를 자체 리포지토리에서 실행한 결과를 보여주며, “빠른 종료”는 Git 리포지토리가 없는 디렉터리에서 도구를 실행해 오류 메시지와 함께 즉시 종료되는 경우입니다.
| 방식 | 빠른 종료 | 실제 워크로드 |
|---|---|---|
| 일반 JVM | 29.8 ms | 280.5 ms |
| AOT 코드가 없는 AOT 캐시 | 25.2 ms | 195.1 ms |
| AOT 코드가 있는 AOT 캐시(JEP 544) | 26.1 ms | 131.6 ms |
| GraalVM 네이티브 이미지 | 3.4 ms | 50.1 ms |
JEP 544는 아직 JDK 28 얼리 액세스 빌드에 포함되지 않았으므로, AOT 코드 수치는 Project Leyden 얼리 액세스 빌드로 측정되었습니다.
워크로드 열의 경우 JVM을 2코어에 고정했기 때문에 JIT 컴파일러가 애플리케이션과 CPU를 두고 경쟁합니다. 이 설정은 JEP 544 성능 그래프가 측정된 환경이기도 하며, 캐시된 AOT 코드가 실질적인 효과를 발휘하는 바로 그 환경입니다: AOT 코드가 없는 캐시에 비해 총 경과 시간(wall time)을 33% 절약하고, 일반 JVM에 비해 53% 절약합니다.


그렇다면 이제 근접했을까요? 아닙니다, GraalVM이 여전히 훨씬 앞서 있습니다. 하지만 격차는 상당히 줄어들고 있습니다. 컨테이너화된 애플리케이션의 시작에서 밀리초가 중요하다면 GraalVM을 선택하세요. 애플리케이션이 클로즈드 월드의 제약을 감당할 수 없다면 AOT를 사용하세요. AOT는 이제 Java 25에서 사용할 수 있으며, Java 28에서는 한층 더 인상적이 될 것입니다.
어떻게 학습시킬까?
AOT 캐시에 관해 남은 질문이 하나 있습니다: 실제로는 캐시를 어떻게 생성할까요? 앱을 사내에 배포한다면 빌드 시스템이나 배포 스크립트에서 학습 실행을 수행할 수 있습니다. 하지만 앱을 출시하는 경우에는 어떤 선택지가 있을까요? 앱이 실제로 실행되는 시스템에서 학습을 수행해야 합니다.
데스크톱 앱의 경우 고객 머신에서 학습 실행을 준비하는 것은 상당히 비현실적으로 들립니다. 학습 실행을 위해서는 특별한 VM 파라미터를 추가해야 하고, 이후 실행에서 기록된 AOT 캐시를 사용하려면 또 다른 VM 파라미터가 필요합니다. AOT 캐시의 속도 향상이 데스크톱 앱의 UX에 매우 중요하긴 하지만, 사용자에게 이런 복잡한 절차에 참여하도록 강요하는 것은 매력적인 선택지가 아닙니다.
이럴 때 install4j의 AOT 캐시 기능이 등장합니다: install4j 런처는 VM 파라미터로 -XX:AOTCacheOutput=<cache file>을 전달하여, 애플리케이션이 처음 시작될 때 선택적으로 AOT 캐시를 기록할 수 있습니다. 이후 실행에서는 -XX:AOTCache=<cache file>로 캐시가 사용됩니다.
기본적으로 캐시는 사용자별 캐시 디렉터리에 저장됩니다. 캐시 파일 이름에는 애플리케이션 빌드와 Java 런타임에서 파생된 키가 포함되므로, 애플리케이션 업데이트나 Java 런타임 변경 후에는 캐시가 자동으로 재생성됩니다. 이전 빌드의 캐시는 자동으로 정리되며, 언인스톨러가 생성된 모든 캐시를 제거합니다.
학습 실행을 프로그래밍 방식으로 종료하고 싶다면, Java 25.0.4부터 제공되는 HotSpotAOTCacheMXBean MBean을 사용하면 됩니다. 이 MBean에서 endRecording()을 호출할 수 있으며, 예를 들어 사용자 인터페이스가 표시되고 초기 백그라운드 작업이 완료된 후에 호출하면 됩니다.


명령줄 애플리케이션의 경우 인스톨러에서 빠른 종료 경로와 함께 학습 실행을 준비하는 것이 가장 좋은 방법입니다. 현재로서는 불가피한 학습 과정의 터미널 출력이 사용자에게 보이는 것을 막을 수 있기 때문입니다. 저희는 install4j 명령줄 컴파일러에 대해 --version 플래그로 실행하여 이렇게 하고 있습니다.
결론
JVM에 가장 불리한 시나리오를 보여드리기 위해, 저는 수치가 GraalVM에 유리하게 나오는 예를 의도적으로 골랐습니다. 그 이유는 앱이 매우 작기 때문입니다. 더 큰 앱에서는 JVM을 띄우지 않아도 된다는 GraalVM의 장점은 상대적으로 덜 중요해집니다. 앱에서 실제 작업이 차지하는 비중이 커지면 이 고정 이득은 비례하여 줄어들고, AOT 코드 캐시가 그 격차를 좁혀 갑니다. 그 정도는 전적으로 어떤 앱을 측정하느냐에 달려 있습니다. 우리 install4j IDE의 경우 AOT 코드가 없는 AOT 캐시만으로도 시작 시간이 50% 단축되는데, 위의 더 작은 명령줄 앱에서는 약 30% 수준입니다.
분명한 것은 AOT 캐시를 적용한 JVM의 시작 시간이 GraalVM과 경쟁할 만큼 좋아지고 있다는 점이며, GraalVM이 요구하는 제약까지 고려하면 더욱 그렇습니다. Java 25는 지금도 훌륭하고, Java 28에서는 이런 면에서 한층 더 나아질 것입니다.
소프트웨어를 출시하면서 install4j의 AOT 캐시 기록 기능으로 시작 시간을 끌어올리고 싶다면, 등록 없이 직접 사용해 보세요.