자바 프로그램의 실행 흐름
.java
파일은 JDK
에 포함된 javac(java compiler)
를 통해 컴파일됩니다. 이 과정에서 JVM
이 이해할 수 있는 바이트 코드로 변환되어 .class
파일이 생성됩니다.
이 후 부터는 JVM
이 담당하는데요. 먼저 클래스 로더(Class Loader)
가 바이트 코드를 JVM
메모리에 동적으로 로드합니다. 로드된 바이트 코드는 Method Area
에 저장되며, 이 때 로딩(Loading)
, 링킹(Linking)
, 초기화(Initialization)
단계를 거칩니다.
그 다음, 실행 엔진(Execution Engine)
이 로드된 바이트 코드를 실행합니다. 하지만 바이트 코드는 컴퓨터가 읽을 수 없기 때문에 인터프리터(Interpreter)
와 JIT 컴파일러(Just-In-Time Compiler)
를 함께 사용하여 기계어로 변환합니다. 인터프리터
는 바이트 코드를 한 줄씩 읽어서 실행하는 방식이고, JIT 컴파일러
는 자주 실행되는 메서드(Hotspot)를 감지하면 해당 메서드 전체를 네이티브 코드로 변환하여 캐싱합니다.
✔️ 클래스 로더
가 바이트 코드를 동적으로 로드한다는 것은 무슨 의미일까?
프로그램이 시작될 때 모든 클래스를 한꺼번에 로드하는 것이 아니라, 런타임 시점에 필요한 클래스만 로드하는 것을 의미합니다. 클래스 로드
는 인스턴스를 생성할 때, static 메서드나 변수를 사용할 때, static 변수에 값을 할당할 때 이루어집니다. 이러한 동적 로드 방식은 불필요한 클래스 로드
를 방지하여 메모리를 효율적으로 사용할 수 있습니다.
✔️ 로딩 (Loading)
클래스 로더
가 .class
파일을 읽어 JVM
메모리에 로드하는 단계입니다. 로드된 클래스는 Method Area
에 저장됩니다.
✔️ 링킹 (Linking)
로드된 클래스가 실행될 수 있도록 준비하는 단계이며 세 가지의 과정으로 이루어집니다.
Verification
.class
파일이 구조적으로 올바른지 확인합니다.
Preparation
static
변수를 메모리에 할당하고 기본 값으로 초기화합니다.
Resolution
런타임 상수 풀에 있는 심볼릭 레퍼런스를 실제 메모리 레퍼런스로 교체합니다.
✔️ 초기화 (Initialization)
static
변수를 사용자가 지정한 값으로 초기화하고 static
블록을 실행하는 단계입니다.
✔️ 실행 엔진이 바이트 코드를 기계어로 변환할 때 인터프리터와 JIT 컴파일러를 함께 사용하는 이유
인터프리터
는 바이트 코드를 한 줄씩 읽어서 실행하는 방식이기 때문에 초기 실행 속도가 빠릅니다. 하지만 같은 코드가 반복적으로 실행될 경우 매번 해석해야 해서 성능이 저하되는 단점이 있습니다. 초기 JVM
은 인터프리터
만 사용했지만, 이러한 단점을 보완하기 위해 JIT 컴파일러
가 도입되었습니다.
JIT 컴파일러는 자주 실행되는 메서드를 네이티브 코드로 변환하여 캐싱합니다. 이렇게 변환된 코드는 반복 실행 시 인터프리터
보다 훨씬 빠르게 실행됩니다. 하지만 JIT
컴파일 과정 자체에 시간이 소요되기 때문에 초기 실행 시 오버헤드가 발생할 수 있습니다. 따라서 JVM
은 두 방식을 함께 사용하여 초기 실행 속도와 높은 반복 실행 성능을 동시에 달성할 수 있습니다.