JVM 인터프리터란? 바이트코드 실행 원리
TOC
Overview
java 명령으로 JVM이 시작되면 JVM은 필요한 클래스 파일을 로드하고 링킹과 초기화를 수행한다. 이후 Execution Engine이 바이트코드 실행을 담당한다.
바이트코드는 CPU가 직접 실행하는 기계어가 아니므로, JVM의 인터프리터는 바이트코드 명령을 하나씩 읽고 해석해 실행한다. 이 글에서는 JVM 인터프리터의 역할과 동작 원리를 살펴본다.
JVM 인터프리터란?
JVM 인터프리터는 .class 파일의 바이트코드 명령을 읽고 해석해 실행하는 JVM 내부 구성 요소다.
인터프리터 자체는 JVM을 구성하는 네이티브 코드로 동작한다. CPU가 바이트코드를 직접 실행하는 것이 아니라, CPU가 실행할 수 있는 인터프리터가 바이트코드 명령에 대응하는 실행 로직을 호출한다.
바이트코드
↓
인터프리터가 명령을 읽고 해석
↓
인터프리터 내부의 네이티브 실행 로직 호출
인터프리터의 동작 원리
메서드가 호출되면 인터프리터는 해당 메서드의 바이트코드를 순서대로 읽고, 각 명령에 맞는 동작을 수행한다.
메서드 호출
↓
Stack Frame 생성
↓
바이트코드 명령 읽기
↓
명령 해석 및 실행
↓
다음 명령으로 이동
인터프리터는 소스 코드의 한 줄을 실행하는 것이 아니라, 컴파일된 바이트코드 명령어 단위로 실행한다.
바이트코드 실행 예시
다음 Java 메서드를 생각해보자.
public int add(int left, int right) {
return left + right;
}
개념적으로 이 메서드는 다음과 같은 바이트코드 명령으로 표현될 수 있다.
iload_1 // left를 피연산자 스택에 적재
iload_2 // right를 피연산자 스택에 적재
iadd // 두 값을 더함
ireturn // 결과를 반환
인터프리터는 위 명령을 하나씩 읽고 실행하며, iadd 명령을 처리할 때 피연산자 스택의 두 정수를 꺼내 더한 뒤 결과를 다시 스택에 저장한다.
인터프리터 실행과 Stack Frame
메서드가 호출되면 JVM은 해당 메서드의 실행 정보를 관리하기 위한 Stack Frame을 생성한다.
Stack Frame에는 지역 변수 배열, 피연산자 스택, 실행에 필요한 참조 정보가 포함된다. 참조 변수가 Frame에 저장되더라도 참조 대상 객체는 Heap에 저장된다.
Stack Frame
├─ 지역 변수 배열
├─ 피연산자 스택
└─ 실행 정보
Heap
└─ 참조 대상 객체
인터프리터의 한계와 JIT
인터프리터는 바이트코드 명령을 실행할 때마다 읽고 해석해야 하므로, 같은 메서드가 반복 실행되면 바이트코드 해석이 반복되어 오버헤드가 발생한다.
JVM의 런타임 프로파일링과 컴파일 정책은 실행 횟수와 실행 정보를 바탕으로 자주 실행되는 메서드나 코드 구간을 Hot Code로 판단한다. 이후 JIT 컴파일러가 해당 코드를 네이티브 코드로 변환하고, 변환된 코드를 재사용한다.
인터프리터로 실행
↓
Hot Code 판단
↓
JIT 컴파일러가 네이티브 코드 생성
↓
변환된 코드 재사용
JIT 컴파일러가 수행하는 컴파일과 최적화 과정은 본문에서 다루지 않는다.
Conclusion
JVM은 로드된 바이트코드를 Execution Engine을 통해 실행한다. 인터프리터는 바이트코드 명령을 하나씩 읽고 해석하며, 반복 실행되는 코드는 JIT 컴파일을 통해 네이티브 코드로 전환될 수 있다.