본문 바로가기

CS/JVM

[JVM 밑바닥까지 파헤치기] JIT vs AOT 컴파일러, 그리고 JVM과 GraalVM에 대하여

기존에 Spring Initializer를 쓰며 GraalVM이란 용어에 대해 본 적이 있었고,

그때는 단순히 이게 뭘까? 라는 생각만 가지고 있었습니다.

 

그러다가 최근 'JVM 밑바닥부터 파헤치기'라는 책을 읽으며 JIT와 AOT 그리고 JVM과 GraalVM에 대략적으로 파악하게 되었습니다.

이 글에서는 이 개념들이 무엇이고, 어떤 차이가 있는지, 그리고 어떻게 선택하면 좋을지 정리해보겠습니다.

 


Java 코드는 어떻게 실행될까?

Java의 실행 과정을 먼저 이해해야 합니다.

.java 파일 → (javac 컴파일) → .class 바이트코드 → (JVM) → 실행

 

Java는 "Write Once, Run Anywhere"를 위해 바이트코드라는 중간 형태로 컴파일됩니다. 이 바이트코드는 JVM 위에서 실행되는데, 여기서 어떻게 실행하느냐에 따라 JIT와 AOT로 나뉩니다.

 


JIT 컴파일러 (Just-In-Time)

JIT란?

JIT는 프로그램 실행 중에 바이트코드를 네이티브 기계어로 변환하는 컴파일러입니다. "Just-In-Time", 즉 "딱 필요한 순간에" 컴파일한다는 의미입니다.

 

동작 방식

  1. 처음에는 인터프리터가 바이트코드를 한 줄씩 해석하며 실행
  2. JVM이 메서드 호출 횟수, 루프 반복 횟수 등을 계속 추적
  3. 자주 실행되는 코드(핫스팟)를 발견하면 네이티브 코드로 컴파일
  4. 이후에는 컴파일된 코드를 직접 실행

 

HotSpot VM의 Tiered Compilation

Oracle JDK에 포함된 HotSpot VM은 두 가지 JIT 컴파일러를 함께 사용합니다.

컴파일러 특징
C1 (Client) 빠른 컴파일, 가벼운 최적화
C2 (Server) 느린 컴파일, 공격적인 최적화

 

실행 단계는 다음과 같이 진행됩니다:

  • Level 0: 인터프리터로 실행
  • Level 1-3: C1 컴파일러가 가벼운 최적화 적용
  • Level 4: 정말 자주 쓰이는 코드는 C2 컴파일러가 최대 최적화

 

JIT가 수행하는 최적화

  • 인라이닝: 작은 메서드를 호출 지점에 직접 삽입
  • 루프 언롤링: 반복문을 펼쳐서 분기 비용 절감
  • 데드 코드 제거: 실행되지 않는 코드 삭제
  • 이스케이프 분석: 객체가 메서드 밖으로 나가지 않으면 힙 대신 스택에 할당

 

JIT의 장단점

장점

  • 런타임 정보를 활용한 공격적인 최적화 가능
  • 특정 분기가 99% 타는 걸 알면 그쪽에 최적화 집중
  • 장시간 실행되는 서버에서 뛰어난 최고 성능(Peak Performance)

단점

  • 시작 시간이 느림 (웜업 필요)
  • 컴파일러가 메모리를 차지함
  • 처음엔 느리다가 점점 빨라지는 예측 어려운 성능

AOT 컴파일러 (Ahead-Of-Time)

AOT란?

AOT는 프로그램 실행 전에 미리 네이티브 기계어로 컴파일하는 방식입니다. C나 Go처럼 실행 파일을 미리 만들어두는 것과 비슷합니다.

 

동작 방식

.java → (javac) → .class → (AOT 컴파일) → 네이티브 실행 파일

빌드 시점에 모든 컴파일이 완료되므로, 실행할 때는 JVM이나 JIT 컴파일러 없이 바로 실행됩니다.

 

AOT의 장단점

장점

  • 시작 속도가 매우 빠름 (웜업 없음)
  • 메모리 사용량 적음 (JIT 컴파일러 불필요)
  • 예측 가능한 일정한 성능
  • 작은 배포 크기

단점

  • 런타임 정보를 활용한 최적화 불가
  • 리플렉션, 동적 프록시 처리가 까다로움
  • 빌드 시간이 오래 걸림
  • 플랫폼별로 별도 빌드 필요

 


JIT vs AOT 비교 정리

구분 JIT AOT
컴파일 시점 실행 중 (런타임) 실행 전 (빌드 타임)
시작 속도 느림 빠름
최고 성능 높음 상대적으로 낮음
메모리 사용 높음 낮음
최적화 수준 런타임 정보 활용 가능 정적 분석만 가능
결과물 메모리에 캐싱 실행 파일

 


JVM과 GraalVM

JVM (Java Virtual Machine)

JVM은 Java 바이트코드를 실행하는 가상 머신입니다. 대표적인 구현체가 HotSpot VM으로, Oracle JDK와 OpenJDK에 포함되어 있습니다.

HotSpot이라는 이름은 자주 실행되는 코드 영역("핫스팟")을 찾아 집중 최적화한다는 의미에서 붙여졌습니다.

 

GraalVM

GraalVM은 Oracle이 개발한 고성능 런타임으로, 기존 JVM을 대체하거나 확장할 수 있습니다.

GraalVM의 두 가지 모드

1. JIT 모드

  • HotSpot의 C2 컴파일러를 Graal 컴파일러로 대체
  • 기존 JVM처럼 동작하면서 더 나은 최적화 제공

2. Native Image (AOT 모드)

  • 애플리케이션을 네이티브 실행 파일로 컴파일
  • JVM 없이 독립 실행 가능
  • 시작 시간 수십~수백 ms로 단축

 

GraalVM의 특징

  • 다국어 지원: Java, JavaScript, Python, Ruby, R 등을 하나의 런타임에서 실행
  • Native Image: AOT 컴파일로 네이티브 실행 파일 생성
  • Polyglot: 여러 언어 간 상호 운용 가능

 


Spring Boot에서 GraalVM Native Image 사용하기

Spring Boot 3부터 GraalVM Native Image를 공식 지원합니다.

 

빌드 방법

Maven

mvn -Pnative native:compile

Gradle

gradle nativeCompile

 

실행 결과 비교

구분 일반 JVM Native Image
시작 시간 2-5초 0.05-0.1초
메모리 200MB+ 50MB 내외
빌드 시간 빠름 느림 (수 분)

 

주의사항

Native Image는 빌드 시점에 @Conditional 어노테이션이 평가됩니다. 즉, 빌드할 때 활성화된 프로파일에 따라 생성된 빈은 런타임에 변경할 수 없습니다.

<!-- Maven에서 AOT 빌드 시 프로파일 지정 -->
<configuration>
    <profiles>profile-a,profile-b</profiles>
</configuration>

 


언제 무엇을 선택해야 할까?

일반 JVM + JIT (HotSpot)을 선택할 때

  • 24/7 운영되는 서버 애플리케이션
  • 장시간 실행되며 최고 성능이 중요한 경우
  • 리플렉션, 동적 프록시를 많이 사용하는 레거시 시스템
  • 빠른 개발 사이클이 필요한 경우

GraalVM Native Image (AOT)를 선택할 때

  • 서버리스 / AWS Lambda: 콜드 스타트가 치명적인 환경
  • CLI 도구: 명령어 실행에 JVM 시작 시간을 기다릴 수 없는 경우
  • 마이크로서비스: 메모리가 제한된 컨테이너 환경
  • 사이드카 패턴: 경량화가 중요한 경우

 


정리

구분 설명
JIT 실행 중 컴파일, 런타임 최적화, 웜업 필요
AOT 실행 전 컴파일, 빠른 시작, 정적 최적화
HotSpot VM 가장 널리 쓰이는 JVM, C1/C2 JIT 컴파일러
GraalVM 고성능 런타임, JIT/AOT 모두 지원, 다국어 지원

결국 어떤 기술을 선택할지는 애플리케이션의 특성과 운영 환경에 따라 달라지는 것 같습니다. 시작 시간이 중요하면 AOT, 장시간 운영되는 서버라면 JIT가 여전히 좋은 선택인 것으로 보입니다.