Jerry's Log

JIT

contents

JIT(Just-In-Time) 컴파일러는 자바를 빠르게 만드는 "핵심 비결"입니다. 자바가 태생적으로는 인터프리터 언어임에도 불구하고, 성능 면에서 C++과 경쟁할 수 있는 이유가 바로 이 기술 덕분입니다.

다음은 JIT가 무엇인지, JVM 내부에서 어떻게 작동하는지, 그리고 어떤 구체적인 최적화를 수행하는지에 대한 상세 분석입니다.

1. 핵심 개념: "통역사 vs 원어민"

JIT를 이해하기 위해서는 자바가 코드를 실행하는 방식을 먼저 이해해야 합니다.

2. JIT 워크플로우: 차가운 상태에서 뜨거운 상태로

JVM(특히 HotSpot JVM)은 "적응형 최적화(Adaptive Optimization)" 라는 프로세스를 사용합니다. 모든 코드를 다 컴파일하는 것이 아니라, 중요한 부분만 골라서 합니다.

  1. 인터프리터 단계 (Cold): 앱이 실행됩니다. JVM은 바이트코드를 인터프리팅합니다. 이 단계에서 성능은 상대적으로 느립니다.
  2. 프로파일링 (Warm-up): 코드가 실행되는 동안, JVM은 모든 메서드와 루프에 대해 카운터(Counter) 를 유지합니다. 다음을 측정합니다:
    • 메서드가 몇 번 호출되었는가?
    • 루프가 몇 번 반복되었는가?
  3. 감지 ("Hot Spot"): 카운터가 특정 임계치(예: 메서드 10,000번 호출)를 넘으면, JVM은 이 코드를 "핫스팟(Hot Spot)" 으로 식별합니다.
  4. 컴파일: JIT 컴파일러가 별도의 백그라운드 스레드에서 실행됩니다. 핫스팟이 된 바이트코드를 가져와서 공격적으로 최적화하고, 네이티브 기계어로 컴파일합니다.
  5. 네이티브 실행: JVM은 해당 메서드의 진입점을 새로운 네이티브 코드로 변경합니다. 이후의 호출은 최고의 속도로 실행됩니다.

3. 계층적 컴파일 (Tiered Compilation): C1과 C2

최신 JVM은 JIT 컴파일러를 하나만 가지고 있지 않고, 두 개를 가지고 있습니다. 이를 계층적 컴파일(Tiered Compilation) 이라 합니다.

생명주기:

코드는 인터프리터에서 시작 $\rightarrow$ C1(빠른 조치)으로 승격 $\rightarrow$ C2(고도로 최적화)로 최종 승격됩니다.


4. JIT의 마법: 최적화 기법들

JIT가 정적 컴파일러(C++의 gcc 같은)보다 특정 상황에서 더 빠를 수 있는 이유가 여기에 있습니다. JIT는 코드가 실행되는 도중 에 컴파일하기 때문에, 코드가 실제로 어떻게 사용되는지 정확히 알고 있습니다.

A. 메서드 인라인화 (Method Inlining - 가장 중요)

JIT는 메서드 호출(Call) 부분을 메서드의 실제 본문(Body) 코드로 교체해 버립니다.

B. 죽은 코드 제거 (Dead Code Elimination)

만약 어떤 코드가 계산한 값을 아무도 사용하지 않는다는 것을 JIT가 깨닫는다면, JIT는 네이티브 코드에서 그 부분을 그냥 삭제해 버립니다.

C. 루프 펼치기 (Loop Unrolling)

루프 조건 검사(i < 1000) 비용을 줄이기 위해, JIT는 루프를 "펼칩니다".

D. 탈출 분석 (Escape Analysis)

JIT는 객체의 범위를 분석합니다. 만약 객체가 메서드 안에서 생성되었는데 절대 밖으로 탈출하지 않는다(static 필드에 할당되거나 반환되지 않음)는 것을 알게 되면, 두 가지 마법을 부립니다:

  1. 스택 할당 (Stack Allocation): 객체를 힙(Heap)이 아닌 스택(Stack)에 할당합니다. 메서드가 끝나면 메모리가 즉시 사라지므로 가비지 컬렉션(GC)이 필요 없습니다!
  2. 스칼라 교체 (Scalar Replacement): 객체를 분해하여 기본 변수(primitive)들로 쪼갠 뒤 CPU 레지스터를 직접 사용합니다.

5. 최적화 해제 (Deoptimization): 안전망

만약 JIT가 가정한 내용이 나중에 틀렸다는 것이 밝혀지면 어떻게 될까요?

6. 요약: 개발자가 알아야 할 점

개념 자바 앱에 미치는 영향
워밍업 (Warm-up) 자바 앱은 처음 켜졌을 때(Cold) 느립니다. 몇 분 지나면 JIT가 "핫스팟"을 최적화했기 때문에 빨라집니다.
코드 캐시 (Code Cache) JIT는 네이티브 코드를 '코드 캐시'라는 특별한 메모리 영역에 저장합니다. 이 공간이 꽉 차면 JIT가 멈추고 성능이 저하될 수 있습니다.
-XX 플래그 대규모 시스템 성능 튜닝 시, JIT 관련 설정을 조절할 수 있습니다 (예: -XX:ReservedCodeCacheSize).

결론:

JIT는 동적이고 학습하는 컴파일러입니다. 실행 초기에 컴파일을 위해 CPU를 조금 더 쓰지만, 그 대가로 장기적으로는 엄청난 성능 향상을 가져다줍니다.

references