오늘은 HotSpot VM과 JIT 컴파일러에 대해서 포스팅을 해보겠습니다.
HotSpot VM
자바 관련 문서들을 읽다 보면 HotSpot VM이라는 용어를 종종 접할 수 있습니다. HotSpot VM에 대해서 살펴보기 전에 우선 HotSpot이 무엇일까요? 단어를 직역하면 '뜨거운 지점'이라고 번역할 수 있지만, 자바에서는 '자주 실행되는 코드 영역'이라는 의미로 사용됩니다. 왜 이름을 이렇게 지었을까요?
HotSpot VM은 원래 Longview Technologies라는 회사에서 개발되었고, 후에 Sun Microsystems가 이 회사를 인수하여 Java에 통합했습니다. 이 VM은 JIT(Just-In-Time) 컴파일러를 포함하고 있으며, 프로그램 실행 중 지속적으로 코드의 성능을 분석합니다. 특히 자주 실행되거나 반복적으로 사용되는 코드(핫스팟)를 식별하고, 이러한 지점을 최적화하여 부하를 최소화하고 높은 성능을 제공합니다.
HotSpot이라는 이름은 이러한 VM의 핵심 동작 방식에서 유래했습니다. HotSpot VM은 자바 1.3 버전부터 기본 VM으로 사용되어 왔으며, 이후 자바 버전에서 지속적으로 다양한 기능과 최적화가 추가되었습니다. 예를 들어, 자바 7에서는 G1 Garbage Collector가 도입되었고, 자바 8에서는 Metaspace가 도입되어 메소드 영역의 메모리 관리가 개선되었습니다. 자바 11 이후로는 Z Garbage Collector (ZGC)와 같은 저지연 GC 알고리즘이 추가되어 대규모 애플리케이션에서도 안정적인 성능을 유지할 수 있게 되었습니다. 또한, 최신 버전에서는 JIT 컴파일러의 개선을 통해 더욱 효율적인 네이티브 코드 생성을 지원하고 있습니다. 이러한 지속적인 기능 추가와 최적화를 통해 HotSpot VM은 현재 운영 중인 대부분의 시스템에서 기본적으로 채택되고 있습니다.
HotSpot VM은 다음의 핵심 구성 요소로 구성됩니다.
- Class Loader System: 클래스 파일을 메모리에 로드하고, 링크 및 초기화를 수행합니다.
- Execution Engine: 바이트코드를 해석하거나 네이티브 코드로 변환하여 실행하며, JIT 컴파일러를 포함합니다.
- Runtime Data Areas: 힙, 스택, 메소드 영역 등 VM이 실행 중에 사용하는 다양한 메모리 영역을 관리합니다.
- Memory Management System (Garbage Collector 포함): 힙과 스택 메모리를 관리하고, GC를 통해 불필요한 객체를 제거합니다.
- Thread Management: 자바 스레드의 생성, 동기화, 스케줄링을 관리합니다.
- Native Method Interface: 자바 코드와 네이티브 코드 간의 상호작용을 지원합니다.
HotSpot VM ≠ JVM
HotSpot VM에 대해서 설명한 글을 읽다 보니 이전에 작성한 JVM과 Class가 JVM에 로딩되는 과정 🧩 포스팅 내용과 흡사하다는 것을 알게 되었습니다. 그렇다면 JVM와 HotSpot VM은 동일한 개념일까요?
HotSpot VM이 기본 JVM으로 널리 사용되고 있기 때문에, 종종 두 개념이 동일시되는 경우가 많지만 JVM(Java Virtual Machine)과 HotSpot VM은 동일하지 않습니다. JVM(Java Virtual Machine)은 Java 애플리케이션 실행을 위한 가상 머신의 개념적 명세로, Java 프로그램이 플랫폼에 독립적으로 동작할 수 있도록 정의된 표준입니다. 이 명세는 Java Community Process(JCP)에 의해 관리되며, 이를 구현한 다양한 JVM 구현체가 존재합니다. HotSpot VM은 이러한 JVM 구현체 중 하나로, Java SE(Standard Edition)에서 기본적으로 사용되는 가장 널리 사용되는 JVM 구현체입니다. 이 외에도 JVM 명세를 기반으로 다양한 환경과 요구 사항에 맞춘 구현체들이 존재하며, 아래는 대표적인 JVM 구현체들입니다.
- OpenJ9 (Eclipse Foundation): OpenJ9는 메모리 사용량이 적고 시작 속도가 빠른 특징을 가지고 있어, 특히 클라우드 환경이나 제한된 리소스를 사용하는 시스템에 적합합니다.
- GraalVM (Oracle): GraalVM은 Java뿐만 아니라 JavaScript, Python, Ruby 등 여러 언어를 지원하며, 네이티브 이미지를 생성할 수 있는 기능으로 마이크로서비스 환경에서 강점을 발휘합니다.
- Azul Zulu / Zing (Azul Systems): Azul Systems에서 제공하는 이 JVM은 대규모 애플리케이션에 최적화되어 있으며, 저지연 GC(C4)를 통해 안정적인 성능을 제공합니다.
- Kaffe: Kaffe는 경량 오픈소스 JVM으로, 임베디드 시스템과 같은 간단한 환경에서 Java 프로그램을 실행하기 위해 설계되었습니다.
이처럼 각 JVM 구현체는 특정 요구 사항에 최적화된 성능을 제공하지만, 대부분의 Java 애플리케이션에서는 여전히 기본적으로 HotSpot VM을 사용하고 있습니다.
즉, 쉽게 비유하자면 JVM은 "자동차"라는 개념과 같습니다. 자동차는 엔진, 바퀴, 핸들 등의 기본적인 설계를 따르지만, 다양한 브랜드와 모델이 존재합니다. 이 중 HotSpot VM은 특정 자동차 브랜드(예: 현대, 기아)에 해당하며, 자동차라는 개념(JVM)을 구현한 하나의 대표적인 모델입니다. 따라서 JVM이라는 표준이 다양한 구현체를 허용하는 것처럼, 자동차 개념도 여러 브랜드와 모델로 구현될 수 있습니다.
HotSpot VM Architecture
HotSpot VM의 아키텍처는 모듈화된 구조로 설계되어 있어, 다양한 구성 요소를 레고 블록처럼 끼워 맞춰 사용할 수 있습니다.
HotSpot JVM 런타임은 주로 Garbage Collection(GC) 방식과 JIT(Just-In-Time) 컴파일러를 유연하게 교체하거나 설정할 수 있는 구조를 가지고 있습니다. 이를 위해 JVM 런타임은 다음과 같은 API를 제공합니다.
- JIT 컴파일러용 API : JIT 컴파일러가 JVM과 상호작용하여 바이트코드를 네이티브 코드로 변환하고 최적화할 수 있도록 지원합니다.
- Garbage Collector용 API : 다양한 GC 알고리즘이 JVM의 메모리 관리와 상호작용할 수 있도록 인터페이스를 제공합니다.
또한, JVM 런타임은 다음과 같은 핵심 기능들도 포함하고 있습니다:
- 런처(Launcher): JVM을 시작하고 초기화하는 역할을 담당합니다. java 명령어를 통해 JVM을 실행할 때 이 컴포넌트가 작동합니다.
- 스레드 관리(Thread Management): 애플리케이션의 멀티스레딩을 관리하며, 스레드의 생성, 실행, 종료 등을 제어합니다.
- JNI(Java Native Interface): 자바 애플리케이션이 네이티브 코드(C/C++ 등)와 상호작용할 수 있도록 지원하는 인터페이스를 제공합니다.
이 다이어그램에서 볼 수 있듯이, HotSpot VM 런타임은 다양한 구성 요소들이 독립적으로 교체 가능하도록 설계되어 있습니다. 예를 들어, 필요에 따라 GC 알고리즘을 Serial GC, Parallel GC, G1 GC 등으로 변경할 수 있으며, JIT 컴파일러도 C1(CLI Compiler)과 C2(C2 Compiler) 등으로 설정할 수 있습니다.
JIT(Just-In-Time) Compiler
그렇다면 JIT(Just-In-Time) 컴파일러는 어떤 역할을 할까요? JIT는 '적절한 시간'이라는 의미를 가지고 있으며, JIT 컴파일러는 자바 바이트코드를 실행 가능한 네이티브 코드로 변환하여 프로그램의 실행 속도를 향상시키는 역할을 담당합니다. 이제 JIT 컴파일러의 동작 방식을 자세히 살펴보겠습니다.
- 처음에는 모든 코드를 인터프리터 모드로 실행합니다.
- VM은 메소드 호출 횟수와 루프 반복 횟수를 지속적으로 모니터링합니다.
- 특정 메소드가 자주 호출되어 임계값(threshold)에 도달하면, 해당 메소드를 JIT 컴파일하여 네이티브 코드로 변환합니다.
- 이후 해당 메소드는 인터프리팅 없이 네이티브 코드로 직접 실행되어 성능이 크게 향상됩니다.
매번 JIT로 컴파일을 수행하면 성능 저하가 심하기 때문에 HotSpot VM 은 효율적인 최적화 단계를 거치게 되는데, 이러한 방식은 실행 중 애플리케이션 상태를 지속적으로 분석하고 필요한 부분에만 집중하는 적응형 최적화(adaptive optimization) 전략의 일환입니다. HotSpot VM은 자주 실행되는 코드(핫스팟)만을 선별적으로 최적화하여 전체적인 성능을 극대화합니다.
JIT Optimizer
앞서 살펴본 JIT 컴파일러가 어떻게 최적화를 수행하는지 더 자세히 알아보겠습니다. HotSpot VM의 JIT 컴파일러는 Client(C1) 버전과 Server(C2) 버전으로 나뉘는데, 이는 서로 다른 최적화 전략을 사용하기 위함입니다.
Client 컴파일러(C1)와 Server 컴파일러(C2)
Client 컴파일러(C1)는 빠른 컴파일 속도를 목표로 하여 초기 단계의 애플리케이션 실행 최적화에 적합하며, 주로 데스크톱 애플리케이션에서 사용됩니다. C1은 컴파일 속도가 빠르기 때문에 애플리케이션의 초기 실행 시 지연 시간을 최소화할 수 있습니다.
반면 Server 컴파일러(C2)는 더 높은 최적화 수준을 제공하여 장기 실행되는 서버 애플리케이션에 적합합니다. C2는 C1에 비해 컴파일에 더 많은 시간을 소요하지만, 실행 성능을 크게 향상시킵니다. 서버 환경에서는 애플리케이션이 장시간 실행되므로, 초기 컴파일 시간이 길더라도 전체적인 성능 향상이 중요합니다.
Tiered Compilation
최근 JVM에서는 Tiered Compilation이라는 기능을 통해 C1과 C2 컴파일러의 장점을 결합하고 있습니다. Tiered Compilation은 총 5단계의 최적화 레벨을 제공하여 애플리케이션의 실행 단계를 더욱 세분화된 최적화 전략으로 관리합니다. 이를 통해 초기 실행 속도를 유지하면서도 장기 실행 시 높은 최적화 수준을 달성할 수 있습니다.
Tiered Compilation 단계
- Level 0: 인터프리터 모드
- 바이트 코드를 인터프리터로 직접 실행합니다.
- 초기 단계의 실행 속도가 빠르지만 최적화는 이루어지지 않습니다.
- Level 1: C1 컴파일러, 단순 최적화 적용
- C1 컴파일러를 사용하여 기본적인 최적화를 적용합니다.
- 컴파일 속도가 빠르며, 초기 실행 성능을 개선합니다.
- Level 2: C1 컴파일러, invocation/backedge counters 추가
- 메서드 호출 빈도와 루프 반복 빈도를 추적하기 위해 수행 카운터와 백에지 카운터를 추가합니다.
- 어느 메서드나 루프가 자주 실행되는지를 파악하여 추가 최적화 대상으로 선정합니다.
- Level 3: C1 컴파일러, 완전한 프로파일링 정보 수집
- 메서드 실행 패턴에 대한 상세한 프로파일링 정보를 수집합니다.
- 분기 예측, 인라인화 가능성 등 고급 최적화 정보를 확보합니다.
- Level 4: C2 컴파일러, 수집된 프로파일링 정보를 바탕으로 완전한 최적화 수행
- C2 컴파일러를 사용하여 수집된 프로파일링 정보를 기반으로 최대한의 최적화를 수행합니다.
- 인라인화, 루프 언롤링, 벡터화 등 고급 최적화 기법이 적용됩니다.
Tiered Compilation은 JVM 구현에 따라 단계가 다소 다를 수 있으며, 일부 JVM에서는 추가적인 최적화 단계를 포함할 수도 있습니다.
자바 컴파일 과정과 전통적인 컴파일러의 차이
먼저 일반적인 컴파일 과정과 자바의 컴파일 과정의 차이를 이해할 필요가 있습니다. 전통적인 컴파일러인 C나 C++의 경우, 소스코드에서 객체 파일(object file)을 만들고, 이를 실행 가능한 라이브러리나 실행 파일로 만드는 과정이 한 번만 수행됩니다. 이 과정은 컴파일 시점에 모든 최적화가 이루어지며, 실행 시점에는 이미 기계어로 변환된 상태이기 때문에 추가적인 변환이 필요 없습니다
반면 자바는 `javac` 컴파일러를 사용하여 소스코드를 바이트 코드로 된 `.class` 파일로 변환합니다. JVM은 이 바이트 코드를 실행할 때마다 동적으로 기계어로 변환합니다. 이 과정에서 JIT 컴파일러가 실행되며, 실행 중인 애플리케이션의 실제 사용 패턴에 맞춰 최적화를 수행합니다. 이러한 동적 최적화는 애플리케이션의 실행 성능을 향상시키는 데 중요한 역할을 합니다. 추가로, GraalVM과 같은 AOT(Ahead-Of-Time) 컴파일러도 존재하여 JVM의 동작 방식을 보완합니다. AOT 컴파일러는 애플리케이션 실행 전에 일부 최적화를 수행하여 초기 실행 속도를 더욱 향상시킬 수 있습니다.
HotSpot VM의 최적화 메커니즘
HotSpot VM에서 코드 최적화를 위한 작업은 각 메서드에 있는 카운터를 통해 통제됩니다. 각 메서드에는 호출 카운터 (Invocation Counter)와 백에지 카운터 (Backedge Counter)가 있으며, 특히 백에지 카운터는 메서드 내의 각 루프마다 별도로 존재할 수 있습니다. 하나의 메서드에 여러 개의 루프가 있을 경우, 백에지 카운터도 그 수만큼 존재하게 됩니다.
호출 카운터 (invocation counter) 와 백에지 카운터 (backedge counter)
- 호출 카운터: 메서드가 호출될 때마다 증가하며, 특정 임계값에 도달하면 해당 메서드를 JIT 컴파일 대상으로 고려합니다.
- 백에지 카운터: 루프와 같이 코드의 실행 흐름이 이전 지점으로 되돌아갈 때마다 증가하는 카운터입니다. 제어 흐름 그래프(Control Flow Graph)에서 루프의 헤더 노드로 돌아가는 간선인 백에지를 따라 루프가 반복될 때마다 이 카운터가 증가합니다. 백에지 카운터는 메서드 내 여러 루프의 반복 횟수를 추적하는 데 사용되며, 루프가 애플리케이션의 성능에 큰 영향을 미칠 수 있기 때문에 호출 카운터와 함께 중요한 역할을 합니다. 이를 통해 성능 분석 도구는 병목 현상을 식별하고 최적화가 필요한 부분을 효과적으로 찾아낼 수 있습니다.
현대 JVM에서는 호출 카운터와 백에지 카운터 외에도 Escape Analysis, Scalar Replacement, Lock Coarsening 등 다양한 프로파일링 기법과 최적화 지표가 사용될 수 있습니다. 이러한 기법들은 객체의 할당 및 사용 패턴을 분석하여 메모리 최적화나 동기화 오버헤드 감소 등에 기여합니다.
OSR(On-Stack Replacement)
JIT 최적화 메커니즘 중에서도 특히 중요한 기능 중 하나는 OSR(On-Stack Replacement)입니다. OSR은 현재 실행 중인 메서드의 코드를 최적화된 버전으로 교체할 수 있게 해줍니다. 이는 특히 장시간 실행되는 루프에서 중요한 역할을 합니다.
예를 들어, 루프가 수천 번 이상 반복되는 상황에서 OSR을 통해 최적화된 코드로 실시간 전환함으로써 성능을 크게 향상시킬 수 있습니다.
OSR은 다음과 같은 시나리오에서 유용하게 사용됩니다:
- 초기 루프 실행 시: 인터프리터 모드로 빠르게 실행을 시작한 후, 루프가 일정 횟수 이상 반복되면 최적화된 JIT 코드를 적용합니다.
- 동적 조건 변화 시: 루프 내 조건이 변하여 다른 최적화가 필요해질 때, OSR을 통해 새로운 최적화 전략을 적용할 수 있습니다.
OSR은 단순히 최적화된 코드로 교체하는 것 외에도, 현재 실행 중인 스택 프레임을 유지하면서 새로운 최적화된 버전으로 전환하는 기술입니다. 이를 통해 애플리케이션의 실행 상태를 유지하면서도 최적화된 코드로의 전환이 원활하게 이루어질 수 있습니다.
프로파일링 기반 최적화
JIT 컴파일러는 실행 중인 애플리케이션의 프로파일링 정보를 바탕으로 최적화를 수행합니다. 이 정보는 메서드 호출 빈도, 루프 실행 빈도, 분기 예측 정보 등을 포함하며, 이를 통해 효율적인 최적화 전략을 적용할 수 있습니다. 주요 최적화 기법에는 다음과 같은 것들이 있습니다.
주요 최적화 기법
- 인라인화 (Inlining)
- 자주 호출되는 메서드를 호출 지점에 삽입하여 호출 오버헤드를 줄입니다.
- 루프 언롤링 (Loop Unrolling)
- 루프 반복 횟수를 줄이기 위해 루프 본문을 여러 번 복제합니다.
- 분기 예측 최적화
- 조건문 등의 분기를 예측하여 캐시 효율을 높이고, 파이프라인의 흐름을 최적화합니다.
- 데드 코드 제거 (Dead Code Elimination)
- 실행되지 않는 코드를 제거하여 코드 크기를 줄이고, 캐시 효율을 높입니다.
추가적으로, 최근 JVM에서는 Escape Analysis, Scalar Replacement, Lock Coarsening 등 추가적인 최적화 기법들도 사용되고 있습니다. 이러한 기법들은 객체의 할당 및 사용 패턴을 분석하여 메모리 최적화나 동기화 오버헤드 감소 등에 기여합니다. 예를 들어, Escape Analysis는 객체가 메서드 외부로 탈출하지 않는다면 스택에 할당하여 힙 할당을 피할 수 있게 합니다. Scalar Replacement은 객체의 필드를 개별 변수로 대체하여 메모리 접근을 최소화합니다. Lock Coarsening은 여러 개의 락을 하나로 합쳐 동기화 오버헤드를 줄이는 역할을 합니다.
GraalVM과 AOT 컴파일러
자바의 JIT 컴파일러 외에도 GraalVM과 같은 AOT(Ahead-Of-Time) 컴파일러가 존재하여 JVM의 동작 방식을 보완합니다. GraalVM은 높은 수준의 최적화를 제공하며, 다중 언어 지원과 함께 네이티브 이미지 생성을 통해 애플리케이션의 초기 실행 속도를 더욱 향상시킬 수 있습니다. AOT 컴파일러는 애플리케이션 실행 전에 일부 최적화를 수행하여 초기 실행 속도를 개선하고, 런타임 성능과는 별도로 최적화된 코드를 제공합니다.
GraalVM의 추가 기능
GraalVM은 단순히 네이티브 이미지 생성을 넘어 다양한 기능을 제공합니다.
- 다중 언어 지원: GraalVM은 Java, JavaScript, Ruby, Python, R, LLVM 기반 언어(C/C++) 등을 하나의 런타임에서 실행할 수 있도록 지원합니다. 이를 통해 여러 언어 간의 상호 운용성을 높이고, 다양한 언어로 작성된 모듈을 통합하여 사용할 수 있습니다.
- Polyglot 애플리케이션 개발: 개발자는 GraalVM을 사용하여 여러 언어로 작성된 코드를 하나의 애플리케이션 내에서 원활하게 통합하고 실행할 수 있습니다. 이는 개발 생산성을 높이고, 다양한 언어의 장점을 활용할 수 있게 합니다.
- 고성능 런타임: GraalVM은 고성능의 JIT 컴파일러를 포함하고 있어, 기존 JVM보다 뛰어난 성능을 제공합니다. 특히, 네이티브 이미지 생성을 통해 애플리케이션의 시작 시간을 크게 단축시킬 수 있습니다.
- Native Image: GraalVM의 Native Image 기능은 자바 애플리케이션을 네이티브 실행 파일로 컴파일하여, JVM 없이도 실행할 수 있게 합니다. 이는 클라우드 환경에서의 빠른 시작 시간과 낮은 메모리 사용을 가능하게 하며, 컨테이너화된 애플리케이션에 이상적입니다.
이러한 GraalVM의 추가 기능들은 JVM의 전통적인 한계를 극복하고, 다양한 개발 요구사항에 유연하게 대응할 수 있도록 합니다.
Tiered Compilation과 OSR의 성능 향상
Tiered Compilation과 OSR이 실제 애플리케이션에서 어떻게 성능을 향상시키는지에 대한 구체적인 사례를 살펴보겠습니다.
예를 들어, 대규모 서버 애플리케이션에서 Tiered Compilation을 사용하면 초기 요청 처리 시 인터프리터 모드와 C1 컴파일러의 빠른 컴파일 속도를 활용하여 빠른 응답성을 유지할 수 있습니다. 이후 애플리케이션이 장시간 실행되면서 C2 컴파일러가 더 높은 최적화 수준을 적용하게 되어 전체적인 처리 성능이 향상됩니다. 또한, 빈번하게 반복되는 루프에 대해 OSR을 적용함으로써 루프 내부의 연산을 최적화된 코드로 교체하여 루프 실행 속도를 크게 높일 수 있습니다. 이러한 최적화는 CPU 사용률을 낮추고 응답 시간을 단축시키는 데 기여합니다.
마치며
이번 글에서는 HotSpot VM의 개념부터 아키텍처, 그리고 JIT 컴파일러의 동작 방식까지 자세히 알아보았습니다.
HotSpot VM은 자바 애플리케이션의 성능을 극대화하기 위해 지속적인 코드 분석과 최적화를 수행하며, 특히 자주 실행되는 '핫스팟' 코드를 집중적으로 개선합니다. 이러한 동적 최적화는 애플리케이션의 응답 속도와 효율성을 높이는 데 핵심적인 역할을 합니다.
또한, JVM의 다양한 구현체 중 하나인 HotSpot VM이 표준 JVM으로서 어떻게 널리 사용되고 있는지, 그리고 다른 JVM 구현체들과의 차이점도 알아보았습니다. 이를 통해 JVM이 단순한 가상 머신 이상의 복잡하고 정교한 시스템임을 이해할 수 있었습니다.
최근에는 GraalVM과 같은 최신 기술의 등장으로 JVM의 전통적인 한계를 넘어서는 새로운 가능성이 열리고 있다는 것도 알게 되었습니다.
이번에 HotSpot VM을 깊이 있게 공부하면서 자바 애플리케이션 성능 튜닝에 한 발자국 더 가까워졌음을 느꼈습니다. 다음에는 JVM에서 빠질 수 없는 개념인 GC, 특히 G1 GC에 대해 포스팅해보려고 합니다.
혹시 제가 잘못 이해한 부분이 있거나 추가로 공유하고 싶은 내용이 있으시다면, 댓글로 알려주시면 감사하겠습니다. 😊
참고
- 책 《자바 성능 튜닝 이야기》
'🎨 Language' 카테고리의 다른 글
가비지 컬렉터 G1 GC 🗑️ (3) | 2024.11.17 |
---|---|
JVM과 Class가 JVM에 로딩되는 과정 🧩 (0) | 2024.11.03 |