Be-Developer

JVM 밑바닥까지 파헤치기 : 11. 백엔드 컴파일과 최적화

11. 백엔드 컴파일과 최적화

  • 백엔드 컴파일 : 코드 --> 바이트코드 -(백엔드 컴파일)-> 네이티브 코드
    • JIT, AOT 컴파일러

11.2 JIT 컴파일러

  1. 인터프리터로 해석해 실행.
  2. 핫코드 (자주 실행되는 메서드나 코드블럭을)를 네이티브 코드로 컴파일하고, 최적화.

11.2.1 인터프리터와 컴파일러

  • 인터프리터는 적극적으로 최적화하는 컴파일러의 비상구 역할도 한다. 적극적 최적화의 가정이 무너지는 경우 최적화를 취소하고 다시 인터프리터에 실행을 맡기기도한다.
  • 핫스팟 가상머신의 JIT 컴파일러
    • C1 : 클라이언트 컴파일러
    • C2 : 서버 컴파일러
    • Gral 컴파일러
  • 인터프리터와 컴파일러 사용 모드 조정 옵션
    • -Xint 인터프리터만 사용
    • -Xcomp 컴파일모드로 고정 = 컴파일을 완료한 후 실행, 인터프리터가 실행에 개입된다.
    • 기본은 mixedMode
  • 계층형 컴파일

계층형 컴파일은 JVM HotSpot의 성능 최적화 전략으로, 코드 실행 시 여러 단계를 거쳐 점진적으로 최적화하는 방식입니다. JDK 7부터 도입되었으며, -XX:+TieredCompilation 옵션으로 활성화됩니다(JDK 8부터는 기본 활성화).

계층형 컴파일 단계

HotSpot VM의 계층형 컴파일은 총 5개의 단계(티어)로 구성됩니다:

                                           [더 많은 최적화]
                                                  ↑
┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    티어 0    │→→→│    티어 1    │→→→│    티어 2    │→→→│    티어 3    │→→→│    티어 4    │
│ 인터프리터   │    │   C1 컴파일러 │    │   C1 컴파일러 │    │   C1 컴파일러 │    │   C2 컴파일러 │
│             │    │   제한된 프로 │    │   기본 프로파 │    │   전체 프로파 │    │   최대 최적화 │
│ 프로파일링:  │    │   파일링     │    │   일링       │    │   일링       │    │             │
│    없음     │    │             │    │             │    │             │    │ 프로파일링:   │
│             │    │ 최적화: 최소  │    │ 최적화: 기본  │    │ 최적화: 기본  │    │    없음     │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
   [시작 단계]                                                                 [최종 단계]

각 단계별 특징

  1. 티어 0 (인터프리터)
    • 모든 코드는 처음에 인터프리터로 실행됩니다
    • 성능 모니터링: 없음
    • 최적화: 없음
    • 용도: 초기 실행 및 프로파일링 정보 수집
  2. 티어 1 (C1 컴파일러, 제한적 프로파일링)
    • 간단한 형태의 JIT 컴파일 수행
    • 성능 모니터링: 일부 (메서드 호출 횟수, 분기 카운터)
    • 최적화: 최소 (인라이닝 없음)
    • 용도: 빠른 컴파일과 기본 최적화
  3. 티어 2 (C1 컴파일러, 기본 프로파일링)
    • 성능 모니터링: 일부 (티어 1보다 더 많은 데이터 수집)
    • 최적화: 제한적 (기본적인 인라이닝)
    • 용도: 중간 수준의 최적화
  4. 티어 3 (C1 컴파일러, 전체 프로파일링)
    • 성능 모니터링: 모두 (모든 프로파일링 정보 수집)
    • 최적화: 중간 (더 광범위한 인라이닝, 루프 최적화)
    • 용도: C2 컴파일러를 위한 상세 프로파일링 데이터 수집
  5. 티어 4 (C2 컴파일러)
    • 성능 모니터링: 없음 (프로파일링 정보만 활용)
    • 최적화: 최대 (공격적인 인라이닝, 루프 최적화, 탈출 분석 등)
    • 용도: 장기 실행 애플리케이션의 최대 성능

최적화 전략 및 특징

  • 적응형 최적화(Adaptive Optimization): 코드의 실행 빈도에 따라 최적화 수준을 조정
  • OSR(On-Stack Replacement): 현재 실행 중인 메서드를 최적화된 버전으로 교체
  • 탈최적화(Deoptimization): 최적화 가정이 깨졌을 때 인터프리터 모드로 회귀
  • 프로파일 기반 최적화(Profile-Guided Optimization): 실행 패턴을 기반으로 최적화 결정

최적화 기법

티어 컴파일러 인라이닝 루프 최적화 탈출 분석 락 생략 벡터화
0 인터프리터
1 C1 제한적 제한적
2,3 C1 중간 제한적 기본
4 C2 광범위 고급 고급

코드 실행 경로 예시

일반적인 메서드 실행 경로:

  1. 인터프리터로 시작 (티어 0)
  2. 호출 횟수 임계값 도달 시 C1 컴파일 (티어 1/2/3)
  3. 충분한 프로파일링 데이터 수집 후 C2 컴파일 (티어 4)
  4. 최종 최적화된 네이티브 코드로 실행

자주 실행되지 않는 코드는 인터프리터나 C1 단계에 머물 수 있으며, “핫” 코드만 C2 컴파일러까지 도달합니다.

JVM 옵션 설정

  • -XX:+TieredCompilation: 계층형 컴파일 활성화 (JDK 8+ 기본값)
  • -XX:TieredStopAtLevel=N: N 티어에서 컴파일 중단 (1~4)
  • -XX:CompileThreshold=N: 컴파일 임계값 설정
  • -XX:+PrintCompilation: 컴파일 정보 출력

11.2.2 컴파일 대상과 촉발 조건

컴파일 대상

  1. 여러번 호출되는 메서드 : 메서드 전체
  2. 여러번 실행되는 순환문의 본문 : 메서드 전체
    • 메서드의 실행 진입점이 달라짐.
    • 온스택 치환 : 메서드 스택 프레임에서 치환됨.

촉발 조건 (여러번?)

  1. 샘플 기반 핫스팟 코드 탐지 : 스레드의 호출스택 상단을 샘플링하여 메서드가 자주 발견되는지 확인
    • J9
  2. 카운터 기반 : 메서드와 코드블록에 대한(백엣지) 카운터를 설정
    • 백엣지 : 순환문 경계에서 순환문 처음으로 점프하는것.
    • 핫스팟 VM
    • 기본 문턱값은 클라이언트 모드(c1)에서 1500회, 서버모드에서(c2) 1만회
    • 단위시간당 호출 횟수를 계산하며, 반감기 이후 카운터값을 반 줄인다.

11.2.3 컴파일 과정

클라이언트 컴파일러(C1)

그림참고

서버 컴파일러(C2)

  • 느리지만, 성능최적화 결과물이 훨씬 좋아서 JDK9부터 기본모드가 됨.

11.2.4 실전: JIT컴파일 결과 확인 및 분석

  • 일부 변수를 넣어서 돌리면 컴파일 과정 로그를 받을 수 있음.
  • 컴파일 순서 경향
    1. 클라이언트 컴파일러가 최적화
    2. 서버 컴파일러가 최적화
    3. 1 최적화 취소
    4. 서버컴파일러가 추가 최적화
  • 컴파일 과정 분석은 IGV 기능을 사용해서 볼수있음.
    • 코드를 이루고있는 블럭들의 변화를 단계별로 볼수있음