인터프리터 vs 컴파일러
- 인터프리터: 코드를 한 줄씩 읽어서 즉시 번역하고 실행
- 장점: 전체 코드를 미리 번역할 필요 없이 즉시 실행 → 시작 속도가 빠름
- 단점:
- 매번 번역 과정을 거침 → 같은 코드가 반복될 때 비효율적
- 전체 실행 속도가 느림
- 컴파일러: 코드를 전체적으로 미리 읽어 한 번에 네이티브 코드(기계어)로 변환
- 장점: 번역 과정 없이 네이티브 코드를 바로 실행하므로 실행 속도가 매우 빠름
- 단점: 전체 코드를 번역하는데 시간이 걸림 → 초기 시작이 느림
- 자바는..?
- 1차 컴파일 (javac): 소스 코드 (.java) -> 바이트코드 (.class)
- 자바 소스 코드(.java)는 javac 컴파일러에 의해 컴파일됨
- 컴파일 과정을 통해 바이트코드(.class) 생성
- 바이트코드는 JVM이 이해할 수 있는 중간 언어
- CPU가 직접 이해하고 실행 불가
- JVM 런타임에서의 변환: 바이트코드 (.class) -> 네이티브 코드
- java 명령어로 .class 파일을 실행하면 JVM이 동작
- JVM은 바이트코드를 네이티브 코드로 변환하여 실행
HotSpot JVM
- 인터프리터
- JVM은 처음에 바이트코드를 한 줄씩 읽어서 네이티브 코드로 변환하고 실행
- 컴파일 하지 않아 빠르게 시작됨
- JIT 컴파일러
- JVM은 코드를 실행하면서 자주 사용되는 코드를(Hot Spot) 프로파일링
- 특정 코드가 핫 스팟으로 감지되면, 해당 바이트코드를 네이티브 코드로 컴파일
- 네이티브 코드는 메모리에 캐싱, 이후 동일한 코드가 실행될 때 네이티브 코드가 즉시 실행
JIT(Just-In-Time) 컴파일러
C1 컴파일러 (Client Compiler)
- 목표: 빠른 시작 속도와 응답성
- 특징:
- 컴파일 속도가 매우 빠름
- 애플리케이션 시작 시 초기 성능 향상 목표
- 기본적인 최적화만 수행
- 메서드 인라이닝(inlining)
- 간단한 루프 최적화
- 주로 데스크톱 애플리케이션이나 실행 시간이 짧은 프로그램에 적합
- 역할:
- 계층적 컴파일의 초기 단계
- 인터프리터에 의해 실행되던 코드가 호출 횟수의 임계점을 넘어가면 C1이 네이티브 코드로 컴파일하여 1차적인 성능 향상을 제공
- C2 컴파일러가 사용할 프로파일링 데이터(profiling data)를 수집
C2 컴파일러 (Server Compiler)
- 목표: 높은 성능 (Peak Performance)
- 특징:
- C1 컴파일 단계에서 수집된 프로파일링 데이터를 기반으로 코드 동작을 분석
- C1보다 느리지만 정교한 최적화 수행
- 공격적 메서드 인라이닝(Aggressive Inlining): 더 많은 메서드를 호출 지점에 직접 삽입하여 오버헤드를 제거
- 루프 언롤링(Loop Unrolling): 루프를 풀어써서 분기 예측 실패를 줄이고 실행 속도를 높임
- 탈출 분석(Escape Analysis): 객체가 메서드 외부로 탈출하지 않는 경우, 힙(Heap) 대신 스택(Stack)에 할당하거나 락(lock)을 제거하여 성능을 향상
- 주로 오랜 시간 동안 중단 없이 실행되어야 하는 서버 애플리케이션에 최적화
- 역할:
- 계층적 컴파일의 최종 단계
- C1에 의해 컴파일된 코드 중 핵심 코드를 C1이 수집한 프로파일링 데이터를 기반으로 더 높은 수준의 최적화를 수행
계층적 컴파일 (Tiered Compilation)
도입 전
- -client 또는 -server 명령 옵션을 사용해 JIT 컴파일러를 직접 선택
- client 옵션 (C1 컴파일러)
- 목표: 빠른 시작 속도와 낮은 메모리 사용량
- 적합: 데스크톱 애플리케이션이나 짧게 실행되는 프로그램
- 제한: 복잡한 최적화를 수행하지 않아 장기적인 피크 성능 낮음
- server 옵션 (C2 컴파일러)
- 목표: 장시간 실행되는 애플리케이션의 최고 성능
- 적합: 서버 애플리케이션
- 제한: 컴파일 시간이 오래 걸려 애플리케이션의 시작이 느림
도입 후
- Level 0 (인터프리터):
- 역할: 초기 단계, 바이트코드를 한 줄씩 해석하며 실행
- 정보 수집: 동시에 메서드 호출 횟수, 루프 반복 횟수 등 기본적인 프로파일링 정보 수집
- 목표: 애플리케이션의 빠른 초기 시작을 담당
- Level 1, 2, 3 (C1 컴파일러):
- 진입 조건: 인터프리터에 의해 실행되던 코드가 일정 호출 임계값을 넘어서면 C1 컴파일러가 개입
- 역할:
- 바이트코드를 빠르게 네이티브 코드로 컴파일하여 즉각적인 성능 향상을 제공
- C1은 기본적인 최적화를 수행하며, 동시에 더 상세하고 정확한 프로파일링 정보를 계속 수집
- 레벨: 내부적으로 여러 최적화 레벨(1, 2, 3)이 있어, 프로파일링 정보 수집 정도나 최적화 강도 조절
- Level 4 (C2 컴파일러):
- 진입 조건: C1에 의해 컴파일된 코드 중에서 실행 빈도가 매우 높고, 성능에 큰 영향을 미치는 핵심 코드가 발견되면 C2 컴파일러가 최종적으로 개입
- 역할:
- C1이 수집한 프로파일링 데이터를 기반으로 해당 코드를 다시 컴파일
- 고급 최적화를 수행하여 최상의 장기적 성능(피크 성능)을 달성하는 네이티브 코드 생성
GraalVM
Oracle에서 개발한 고성능 JDK(Java Development Kit) 배포판
- 고성능 JIT(Just-In-Time) 컴파일러 (Graal Compiler):
- GraalVM의 JIT 컴파일러는 자바로 작성 (기존 HotSpot JVM의 C1/C2 컴파일러는 C++)
- 모듈화, 유지보수성, 확장성 향상
- AOT(Ahead-Of-Time) 컴파일 (Native Image):
- Java 바이트코드를 실행하기 전에(Ahead-of-Time) 특정 운영체제와 아키텍처에 맞는 네이티브 실행 파일로 미리 컴파일하는 기술
- 네이티브 실행 파일은 JVM 없이 단독으로 실행 가능
- JVM의 워밍업(JIT 컴파일 시간)이 필요 없어 시작 속도가 매우 빠르고 메모리 사용량이 낮다
- 마이크로서비스나 서버리스 함수처럼 빠른 시작 시간과 낮은 메모리 사용량이 중요한 경우에 유리
- 런타임 정보(프로파일링 데이터)를 활용할 수 없어 JIT 컴파일러가 동적으로 수행하는 최적화 불가능
- 장시간 실행되는 애플리케이션의 최대 피크 성능은 JIT 컴파일 애플리케이션보다 약간 낮을 수 있다
- 다중 언어 지원 (Polyglot Programming) 및 Truffle 프레임워크:
- 자바뿐만 아니라 자바스크립트, 파이썬, 루비, R, WebAssembly, C/C++와 같은 언어들을 하나의 런타임 환경에서 실행하고 상호 운용할 수 있도록 지원