CS/JAVA

JVM 한눈에 보기: Interpreter, JIT, Inline, Safepoint까지

dding-shark 2026. 3. 11. 16:34
728x90

JVM  한눈에 보기: Interpreter, JIT, Inline, Safepoint까지

들어가며

앞 글에서는 JVM을 전체 런타임 시스템으로 보고,
클래스 로딩과 메모리 구조를 중심으로 큰 그림을 정리했다.

그 흐름을 아주 단순하게 요약하면 이렇다.

  • ClassLoader가 클래스를 메모리에 올린다
  • Runtime Data Areas가 실행 중 필요한 데이터를 저장한다
  • Stack Frame 위에서 메서드가 실행된다
  • Heap에는 객체가 생성된다

그런데 여기서 자연스럽게 다음 질문이 생긴다.

  • 실제 바이트코드는 누가 실행하는가
  • JVM은 항상 한 줄씩 해석만 하는가
  • 자주 실행되는 코드는 왜 빨라지는가
  • 메서드 호출은 어떻게 최적화되는가
  • GC나 스레드 덤프는 실행 중인 스레드를 어떻게 멈추는가

이 질문에 답하려면 JVM의 Execution Engine을 봐야 한다.

이번 글은 JVM 실행 엔진을 다음 흐름으로 정리한다.

  • 바이트코드는 어떻게 실행되는가
  • Interpreter와 JIT는 어떤 관계인가
  • HotSpot은 무엇을 뜻하는가
  • 메서드 호출은 어떻게 최적화되는가
  • Inline, OSR, Safepoint는 어디서 등장하는가

1) Execution Engine은 무엇을 하는가

Execution Engine은 JVM 안에서 바이트코드를 실제로 실행하는 영역이다.

앞 글에서 Runtime Data Areas가 실행에 필요한 데이터를 저장하는 구조였다면,
Execution Engine은 그 데이터를 바탕으로 실제로 명령을 읽고 수행하는 주체라고 보면 된다.

아주 단순화하면 아래처럼 생각할 수 있다.

핵심은 Execution Engine이 단순한 "실행기" 하나가 아니라는 점이다.
실제 JVM, 특히 HotSpot JVM에서는 아래 요소들이 함께 연결된다.

  • Interpreter
  • JIT Compiler
  • Profiling
  • HotSpot Detection
  • Tiered Compilation
  • Deoptimization
  • Safepoint

즉, Execution Engine은 바이트코드를 읽는 것에서 끝나지 않고,
어떤 코드를 얼마나 자주 실행하는지 관찰하고, 더 빠른 방식으로 바꿔 실행하는 최적화 시스템까지 포함한다.


2) 바이트코드는 어떻게 실행되는가

2-1) 처음에는 Interpreter로 시작한다

Java 코드는 javac를 거쳐 .class 바이트코드가 되고, JVM은 이 바이트코드를 실행한다.

그런데 중요한 점이 하나 있다.
JVM은 처음부터 모든 바이트코드를 곧바로 고도로 최적화된 기계어로 바꾸지 않는다.

일반적인 흐름은 아래와 가깝다.

즉 처음에는 인터프리터가 바이트코드를 읽어서 실행하고,
실행 중 수집된 정보를 바탕으로 JIT 컴파일러가 일부 코드를 기계어로 바꿔 성능을 높인다.

예를 들어 아래 같은 메서드가 있다고 하자.

int sum(int a, int b) {
    return a + b;
}

바이트코드 수준에서는 대략 아래처럼 표현될 수 있다.

iload_1
iload_2
iadd
ireturn

인터프리터는 이 명령을 순서대로 읽고 실행한다.

이 방식의 장점은 분명하다.

  • 시작이 빠르다
  • 컴파일 비용이 없다
  • 처음 보는 코드를 바로 실행할 수 있다

하지만 단점도 있다.

  • 실행할 때마다 명령을 해석해야 한다
  • 반복적으로 호출되는 코드에서는 오버헤드가 누적된다

즉, 인터프리터는 스타트업에는 유리하지만 장기 실행 성능에는 불리할 수 있다.

2-2) 왜 JIT Compiler가 필요한가

JVM이 항상 인터프리터만 사용한다면, 반복적으로 실행되는 코드는 매번 같은 바이트코드를 해석해야 한다.

예를 들어 아래 같은 코드는 인터프리터만으로는 비효율이 커질 수 있다.

  • 자주 호출되는 메서드
  • 긴 루프 안의 연산
  • 반복적으로 등장하는 메서드 디스패치

이 문제를 해결하기 위해 JVM은 JIT(Just-In-Time) Compiler를 사용한다.

JIT의 핵심 아이디어는 단순하다.

  • 실행 중 자주 쓰이는 코드를 찾는다
  • 그 코드를 기계어로 컴파일한다
  • 이후에는 더 빠르게 실행한다

즉, JIT는 프로그램 시작 전에 전부 컴파일하는 것이 아니라, 실행 중 필요한 코드만 골라서 컴파일하는 방식이다.

2-3) HotSpot과 Profiling

JIT를 이해할 때 가장 먼저 나오는 단어가 HotSpot이다.

HotSpot은 말 그대로 자주 실행되는 지점을 뜻한다.
보통 아래 두 가지가 대표적이다.

  • 자주 호출되는 메서드
  • 자주 반복되는 루프

즉 JVM은 프로그램 전체를 똑같이 바라보지 않는다.
모든 코드를 한 번에 공격적으로 최적화하는 대신, 실제로 많이 실행되는 부분에만 비용을 집중한다.

흐름을 단순화하면 아래와 같다.

이때 단순히 "많이 실행됐다"는 사실만 보는 것은 아니다.
JVM은 실행 과정에서 다양한 프로파일 정보(profile data)를 수집한다.

대표적으로 아래 같은 정보가 중요하다.

  • 어떤 메서드가 자주 호출되는가
  • 특정 호출 지점에서 실제로 어떤 타입이 자주 들어오는가
  • 분기문이 주로 어느 방향으로 가는가
  • 어떤 루프가 오래 반복되는가

이런 정보는 나중에 JIT 최적화의 근거가 된다.

예를 들어 어떤 메서드 호출 지점에서 항상 같은 타입만 들어온다면,
JIT는 그 호출을 더 공격적으로 최적화할 수 있다.

참고로 HotSpot JVM이라는 구현체 이름도,
자주 실행되는 hotspot 코드 영역을 집중 최적화한다는 아이디어와 연결된다.

2-4) Tiered Compilation

JVM을 처음 공부할 때는 종종 이렇게 이해하기 쉽다.

  • Interpreter는 느린 방식
  • JIT는 빠른 방식
  • 결국 Interpreter를 버리고 JIT로 가는 것

하지만 실제 HotSpot JVM에서는 이 둘이 단순 대체 관계라기보다 단계적으로 협력하는 구조에 가깝다.
이를 보통 Tiered Compilation이라고 부른다.

아주 단순화하면 흐름은 아래와 같다.

개념적으로는 이렇게 이해하면 된다.

  • Interpreter: 빠르게 시작하고 프로파일을 모은다
  • C1 Compiler: 비교적 빠르게 컴파일하면서 적당한 최적화를 한다
  • C2 Compiler: 더 많은 정보를 바탕으로 고급 최적화를 수행한다

즉, JVM은 처음부터 무거운 최적화를 다 거는 것이 아니라 점점 더 좋은 코드로 승격시키는 전략을 사용한다.

이 구조의 장점은 명확하다.

  • 시작 속도를 해치지 않으면서
  • 장기 실행 성능도 챙길 수 있다

3) 메서드 호출은 어떻게 빨라지는가

3-1) 왜 method call이 중요한가

JVM 실행 최적화에서 가장 중요한 주제 중 하나가 메서드 호출(method call)이다.

왜냐하면 실제 프로그램은 단순 계산보다도 아래 작업이 훨씬 빈번하게 일어나기 때문이다.

  • 메서드 호출
  • 객체 참조
  • 동적 디스패치
  • 인터페이스 호출

특히 Java는 객체지향 언어라서, 아래처럼 보이는 평범한 호출도 내부적으로는 단순하지 않을 수 있다.

animal.sound();

이 호출은 컴파일 시점에 "무조건 Dog.sound()"처럼 확정되지 않을 수도 있다.
실행 시점의 실제 객체 타입에 따라 다른 메서드가 호출될 수 있기 때문이다.

3-2) Virtual Call은 왜 바로 점프하지 못할 수 있는가

자바의 가상 메서드 호출은 virtual dispatch를 거칠 수 있다.

예를 들어 아래를 보자.

Animal animal = new Dog();
animal.sound();

소스 코드에서 보이는 타입은 Animal이지만, 실제 객체는 Dog일 수 있다.
JVM은 실행 시점에 실제 타입을 확인해야 한다.

이때 객체는 내부적으로 자기 클래스 정보를 가리키는 포인터를 갖고 있고,
그 클래스 메타데이터 쪽에 메서드 디스패치에 필요한 정보가 연결된다.

개념적으로 단순화하면 아래처럼 볼 수 있다.

여기서 중요한 개념은 다음과 같다.

  • 객체는 자신이 어떤 클래스의 인스턴스인지 알아야 한다
  • 그 정보는 클래스 메타데이터로 연결된다
  • 가상 메서드 호출은 이 메타데이터를 따라 실제 대상 메서드를 찾는다

즉, virtual call은 단순한 정적 함수 호출보다 비용이 더 들 수 있다.

가상 호출을 설명할 때 vtable, itable 같은 용어가 자주 나오는데,
이것은 이해를 돕기 위한 전형적인 설명 방식이라고 보면 된다.
실제 내부 표현은 JVM 구현체에 따라 달라질 수 있다.

3-3) Call Site: 같은 호출 지점도 패턴이 다르다

JIT 최적화를 이해할 때 매우 중요한 개념이 call site다.

call site는 말 그대로 "메서드를 호출하는 지점"이다.
예를 들어 코드에서 같은 animal.sound() 한 줄이 하나의 call site가 된다.

문제는 모든 call site가 똑같지 않다는 것이다.

  • 어떤 호출 지점은 항상 같은 타입만 들어온다
  • 어떤 호출 지점은 두세 타입이 섞인다
  • 어떤 호출 지점은 아주 다양한 타입이 들어온다

이 차이를 보통 아래처럼 분류한다.

  • Monomorphic: 한 가지 타입만 들어오는 호출 지점
  • Bimorphic: 두 가지 타입이 주로 들어오는 호출 지점
  • Megamorphic: 여러 타입이 섞여 들어오는 호출 지점


이 분류가 중요한 이유는, JIT 입장에서 최적화하기 쉬운 호출과 어려운 호출이 갈리기 때문이다.

3-4) Inline: 메서드 호출 자체를 없애는 최적화

JIT 최적화에서 가장 중요한 주제 중 하나가 inline이다.

inline은 단순히 "빠르게 호출한다"가 아니다.
더 정확히 말하면, 메서드 호출을 없애고 호출 대상의 코드를 호출 위치에 직접 펼쳐 넣는 최적화다.

예를 들어 아래 코드가 있다고 하자.

int plusOne(int x) {
    return x + 1;
}

그리고 이것을 반복 호출한다면, JIT는 내부적으로 개념적으로 아래처럼 바꿀 수 있다.

// before
int y = plusOne(a);

// after conceptually
int y = a + 1;

이렇게 되면 단순히 호출 비용만 줄어드는 게 아니다.
inline 이후에는 더 많은 최적화 기회가 열린다.

예를 들면:

  • 불필요한 매개변수 전달 제거
  • 상수 전파
  • 분기 제거
  • 범위 체크 제거
  • 추가적인 dead code elimination

즉, inline은 그 자체가 최적화이기도 하지만, 더 큰 최적화의 출발점이기도 하다.

3-5) Inline은 언제 잘 되는가

JIT가 모든 메서드를 무조건 inline하는 것은 아니다.
inline도 비용과 이득을 따져가며 수행한다.

대표적으로 아래 조건이 중요하다.

  • 메서드가 충분히 작다
  • 호출 빈도가 높다
  • 프로파일이 안정적이다
  • call site가 monomorphic에 가깝다

즉, 아래 같은 경우가 인라인에 유리하다.

  • 자주 호출되는 작은 getter/setter
  • 반복문 안의 단순 메서드
  • 실제 타입이 거의 고정된 가상 호출

반대로 아래는 인라인이 어려울 수 있다.

  • 메서드가 너무 크다
  • 예외 흐름이 복잡하다
  • 호출 대상 타입이 너무 다양하다
  • 코드 크기 증가가 과도하다

3-6) Inlining과 Dynamic Dispatch는 어떻게 연결되는가

자바의 메서드 호출이 동적 디스패치를 포함할 수 있다고 했는데,
그렇다면 가상 메서드는 인라인이 아예 어려운 걸까?

그렇지는 않다.

중요한 것은 "문법상 virtual call인가"보다, 실행 중 관찰된 패턴이 얼마나 안정적인가다.

예를 들어 문법적으로는 다형적 호출이어도
실행 중에는 실제 타입이 늘 하나로 고정되어 있다면, JIT는 이를 적극적으로 최적화할 수 있다.

흐름을 단순화하면 아래와 같다.

즉, JVM 최적화는 소스 코드에 드러난 구조만 보는 것이 아니라,
실행 중 실제 타입 분포를 바탕으로 가상 호출조차 더 빠르게 바꿀 수 있다는 점이 핵심이다.


4) 실행 중 최적화와 안전성은 어떻게 연결되는가

4-1) OSR: 루프를 돌다가 중간에 더 빠른 코드로 갈아탄다

JIT를 공부하다 보면 OSR(On-Stack Replacement)라는 용어가 나온다.

이 개념은 처음 보면 조금 낯설지만, 아이디어 자체는 단순하다.

보통 인터프리터는 먼저 코드를 실행하고, JIT는 그중 자주 실행되는 부분을 나중에 컴파일한다고 했다.

그런데 긴 루프가 이미 한참 돌고 있다면 어떨까?

  • 처음엔 인터프리터로 실행을 시작했는데
  • 루프가 계속 반복되면서 hot loop가 되었고
  • 이제는 JIT 컴파일할 가치가 생겼다

이때 메서드 전체가 끝날 때까지 기다리지 않고,
실행 중인 루프 한가운데서 더 빠른 컴파일 코드로 갈아타는 것이 OSR이다.


즉, OSR은 "이미 실행 중인 코드"를 더 빠른 경로로 옮겨 타는 기술이다.

특히 반복문이 긴 코드에서는 이 기능이 중요하다.
메서드가 끝난 뒤에야 최적화 결과를 적용하면 너무 늦을 수 있기 때문이다.

4-2) Safepoint: JVM은 언제 모든 스레드를 멈출 수 있는가

JVM을 공부하다 보면 GC, thread dump, class redefinition 같은 주제에서 Safepoint가 자주 등장한다.

Safepoint는 아주 거칠게 말하면, JVM이 특정 작업을 안전하게 수행하기 위해
실행 중인 스레드들이 도달해야 하는 안전한 중단 지점이라고 볼 수 있다.

대표적으로 safepoint가 중요한 상황은 아래와 같다.

  • GC
  • thread dump
  • 일부 런타임 재구성 작업
  • 특정 락 상태 정리
  • 클래스 재정의 관련 작업

핵심은 JVM이 아무 때나 스레드를 강제로 멈추는 것이 아니라,
상태가 일관되게 관찰 가능한 지점에서 멈추려 한다는 점이다.

Safepoint는 보통 무작위 위치에 있는 것이 아니라,
JVM이 의미 있는 실행 경계로 삼는 지점과 연결된다.

대표적으로 아래가 자주 언급된다.

  • method call
  • loop back edge
  • return

이를 아주 단순화하면 아래와 같이 볼 수 있다.

이때 자주 오해하는 점이 하나 있다.

Safepoint는 "무조건 모든 명령마다 검사한다"는 뜻이 아니다.
그렇게 하면 오히려 실행 비용이 너무 커질 수 있다.
그래서 JVM은 비용과 안전성 사이에서 적절한 지점을 골라 검사한다.

4-3) Safepoint와 Stop-The-World는 어떤 관계인가

Safepoint를 설명할 때 자주 같이 나오는 말이 Stop-The-World(STW)다.

둘은 같은 말은 아니지만 강하게 연결된다.

  • Safepoint: 스레드가 안전하게 멈출 수 있는 지점
  • Stop-The-World: JVM이 애플리케이션 실행을 전반적으로 멈추는 상태

예를 들어 GC를 수행해야 할 때 JVM은 각 스레드가 safepoint에 도달하도록 하고,
필요한 시점에 전체 애플리케이션 스레드를 멈춘 뒤 작업을 진행할 수 있다.

즉, safepoint는 STW를 가능하게 만드는 실행 경계 개념이라고 이해하면 된다.

다만 모든 GC 작업이 항상 전부 STW로만 수행되는 것은 아니다.
collector에 따라 concurrent phase를 함께 사용하는 경우도 있다.

4-4) Deoptimization: 최적화는 항상 맞다고 보장되지 않는다

JIT는 실행 중 수집한 정보를 바탕으로 매우 공격적인 최적화를 한다.
하지만 그 최적화는 어디까지나 현재까지 관찰된 패턴이 앞으로도 유지될 것이라는 가정 위에 서 있다.

예를 들어:

  • 어떤 call site가 항상 Dog 타입만 받는다고 믿고 인라인했는데
  • 나중에 Cat 타입이 실제로 들어오기 시작한다면

기존 최적화 가정이 깨질 수 있다.

이때 JVM은 잘못된 최적화 코드를 계속 고집하지 않고,
필요하면 더 일반적인 실행 방식으로 되돌아간다.
이것이 deoptimization이다.

흐름을 단순화하면 아래와 같다.

즉 JVM 최적화의 핵심은 "무조건 빠른 코드"가 아니라,
관찰된 현실에 맞춰 최적화하고 필요하면 다시 물러날 수 있는 적응형 실행 모델이라는 점이다.


5) Execution Engine을 한 흐름으로 묶어 보면

지금까지 나온 내용을 하나로 연결하면 대략 이런 그림이 된다.

이 흐름을 문장으로 풀면 이렇다.

  1. 클래스가 로드되고 바이트코드가 준비된다.
  2. 처음에는 인터프리터가 코드를 실행한다.
  3. 실행 중 JVM은 호출 빈도, 타입 분포, 루프 패턴 등을 관찰한다.
  4. 자주 실행되는 코드가 감지되면 JIT 컴파일러가 더 빠른 코드로 바꾼다.
  5. 필요하면 긴 루프 도중에도 OSR로 컴파일 코드에 진입한다.
  6. 최적화 가정이 깨지면 deoptimization으로 더 일반적인 실행 경로로 돌아간다.
  7. GC나 런타임 작업을 위해 safepoint 체크가 수행된다.

즉, Execution Engine은 단순히 바이트코드를 읽는 엔진이 아니라,
실행하면서 관찰하고, 선택적으로 최적화하고, 필요하면 되돌아가며, 전체 런타임 안정성까지 관리하는 적응형 시스템이다.


마치며

JVM 실행 엔진을 볼 때 중요한 것은 개별 용어를 따로 외우는 것이 아니다.

  • Interpreter
  • JIT
  • Profiling
  • Inline
  • OSR
  • Safepoint
  • Deoptimization

이 각각을 별개 기능처럼 보기보다, 실행 중 성능과 안정성을 동시에 확보하기 위한 하나의 흐름으로 보는 것이 훨씬 중요하다.

정리하면:

  • 처음에는 Interpreter가 바이트코드를 바로 실행한다
  • JVM은 실행하면서 어떤 코드가 중요한지 관찰한다
  • HotSpot이 감지되면 JIT가 더 빠른 기계어 코드를 만든다
  • 메서드 호출은 profiling과 inlining을 통해 크게 최적화될 수 있다
  • 긴 루프는 OSR로 실행 중간에 더 빠른 코드로 전환될 수 있다
  • Safepoint는 GC와 런타임 작업을 위한 안전한 중단 지점이다
  • 최적화 가정이 깨지면 deoptimization으로 유연하게 되돌아간다

한 줄로 요약하면 아래와 같다.

JVM Execution Engine은 바이트코드를 단순히 실행하는 장치가 아니라, 실행 패턴을 관찰해 점점 더 빠른 코드로 바꾸고 필요하면 다시 되돌아갈 수 있는 적응형 런타임 시스템이다.


다음 글 예고

이 글 다음에는 아래 주제 중 하나로 이어가면 흐름이 좋다.

  • GC 한눈에 보기
  • JIT 최적화 심화편
  • Safepoint, STW, GC 로그를 연결한 운영 관점 글

부록

부록 A) Inline Cache와 Call Site 최적화

call site를 이해할 때 같이 보면 좋은 개념이 inline cache다.

핵심 아이디어는 단순하다.

  • 어떤 호출 지점에서
  • 실제로 자주 들어오는 receiver type을 기록하고
  • 다음 호출 때 더 빠르게 대상 메서드에 접근하는 것이다

예를 들어 어떤 call site가 계속 Dog만 받는다면, JVM은 이 호출 지점에서 "대부분 Dog.sound()로 간다"는 사실을 활용할 수 있다.

흐름을 단순화하면 아래와 같다.

monomorphic call site가 최적화하기 좋은 이유도 여기와 연결된다.

  • 타입이 하나로 고정되기 쉽다
  • 캐시가 안정적이다
  • direct call에 가까운 형태로 바꾸기 쉽다
  • 이후 inline으로 이어지기 쉽다

반대로 megamorphic call site는 receiver type이 너무 다양해서 캐시와 인라인 최적화가 훨씬 어렵다.

부록 B) Method Data와 Profiling 정보는 어디에 쌓이는가

JVM이 "이 메서드가 자주 호출된다" 또는 "이 call site에는 Dog가 자주 온다"는 사실을 알려면,
실행 중 수집한 정보가 어딘가에 저장되어야 한다.

HotSpot 관점에서는 흔히 MethodData 또는 MDO(MethodDataOop) 같은 개념으로 설명한다.

입문 수준에서는 아래 정도로 이해하면 충분하다.

  • 호출 횟수
  • 루프 카운터
  • 분기 패턴
  • 타입 프로파일

이 정보는 이후 JIT가 아래 같은 결정을 내리는 근거가 된다.

  • 지금 컴파일할 가치가 있는가
  • 이 call site는 monomorphic에 가까운가
  • 어떤 분기가 hot path인가
  • 어떤 메서드가 inline 후보인가

즉, 프로파일 정보는 "실행 엔진의 관찰 노트" 같은 역할을 한다고 보면 된다.

부록 C) C1, C2, Tiered Compilation의 실제 역할

Tiered Compilation을 조금 더 구체적으로 보면 C1C2는 성격이 다르다.

구성 요소 역할
Interpreter 빠른 시작, 초기 실행, 프로파일 수집 시작
C1 비교적 빠른 컴파일, 가벼운 최적화, 빠른 승격
C2 더 무거운 컴파일, 공격적인 최적화, 장기 성능 지향

이 구조의 핵심은 "처음부터 최고 최적화"가 아니라는 점이다.

  • 빠른 시작이 필요하다
  • 곧바로 무거운 컴파일을 걸면 스타트업이 나빠진다
  • 실제로 오래 도는 코드만 나중에 더 비싼 최적화를 적용하면 된다

그래서 tiered compilation은 속도와 비용 사이의 균형 전략으로 이해하는 것이 좋다.

부록 D) JIT가 만든 코드는 어디에 저장되는가: Code Cache와 nmethod

JIT가 메서드를 기계어로 컴파일했다면, 그 결과 코드는 어디엔가 저장되어야 한다.

HotSpot에서는 흔히 이 영역을 Code Cache라고 부른다.

그리고 JIT가 만든 컴파일 결과는 메서드 단위로 nmethod 형태로 설명되는 경우가 많다.

아주 단순화하면:

  • 바이트코드는 .class 쪽에 있다
  • JIT가 컴파일한 결과는 code cache 쪽에 있다
  • 이후에는 해당 기계어 코드를 직접 실행할 수 있다

이 개념이 중요한 이유는, JVM이 "인터프리터 -> 컴파일 코드"로 전환할 때 실제로는 이미 만들어 둔 기계어 코드를 재사용하기 때문이다.

또 deoptimization, code cache 관리, 재컴파일 같은 주제도 이 영역과 연결된다.

부록 E) OSR은 왜 Loop Back Edge에서 일어나는가

OSR을 설명할 때 loop back edge라는 표현이 자주 등장한다.

이건 루프가 한 번 더 반복되기 위해 뒤로 되돌아가는 지점을 뜻한다.

왜 이 지점이 중요할까?

  • 루프는 반복 횟수가 많아지기 쉽다
  • hot loop 판단이 여기서 잘 드러난다
  • 현재 실행 상태를 새 컴파일 코드로 넘기기 좋은 경계가 된다

즉, JVM은 긴 루프가 충분히 뜨거워졌다고 판단하면,
loop back edge 부근에서 OSR 진입을 시도해 인터프리터 실행을 컴파일 코드로 바꿀 수 있다.

핵심은 이거다.

OSR은 메서드 처음부터 다시 시작하는 것이 아니라, 이미 돌고 있는 실행 상태를 적절한 지점에서 컴파일 코드로 옮겨 탄다.

부록 F) Deoptimization과 Uncommon Trap

JIT는 hot path를 빠르게 만들기 위해 종종 "드물게 일어나는 경우"를 바깥으로 밀어낸다.

이때 자주 나오는 개념이 uncommon trap이다.

아이디어는 단순하다.

  • 평소 거의 일어나지 않는 경로는 hot path에서 치운다
  • 자주 일어나는 경로만 공격적으로 최적화한다
  • 그런데 드문 경로가 실제로 발생하면 일반 실행 경로로 빠져나간다

예를 들어:

  • 어떤 call site는 거의 항상 Dog만 온다
  • 그래서 Dog 기준으로 빠르게 최적화한다
  • 그런데 갑자기 Cat이 들어오면 기존 가정이 깨진다

이때 JVM은 deoptimization을 수행해 더 일반적인 코드로 되돌아갈 수 있다.

즉:

  • Uncommon Trap: 드문 경로를 따로 빼두는 전략
  • Deoptimization: 최적화 가정이 깨졌을 때 더 일반적인 실행 방식으로 돌아가는 메커니즘

둘은 함께 이해하면 좋다.

부록 G) Safepoint Poll은 어떻게 스레드를 멈추게 하는가

Safepoint를 조금 더 깊게 보면 자주 나오는 개념이 safepoint poll이다.

JVM은 모든 명령마다 "멈춰야 하나?"를 검사하지 않는다.
대신 특정 실행 경계에서 poll 또는 check를 수행한다.

대표적으로:

  • method call 근처
  • loop back edge
  • return 경계

이 지점에서 JVM은 "지금 safepoint 요청이 있는가?"를 확인할 수 있다.

흐름을 단순화하면 아래와 같다.

즉 safepoint는 "어디선가 갑자기 강제로 멈춘다"기보다,
JVM이 정해둔 안전한 경계에서 멈춤 가능성을 점검하는 구조로 이해하는 것이 더 정확하다.

728x90