JVM(Java Virtual Machine)은 자바 가상 머신으로, Java 프로그램을 실행하기 위한 가상 컴퓨터입니다. 즉, Java로 작성된 바이트코드(bytecode)를 운영체제가 이해할 수 있는 명령으로 해석하고 실행해 주는 소프트웨어 엔진입니다.
이 가상 머신이 실제 하드웨어와 운영체제 사이에서 중개자 역할을 하여, Java 프로그램이 어떤 운영체제에서든 동일하게 실행될 수 있는 실행 환경을 제공합니다. 한마디로, JVM은 Java 프로그램을 구동시키기 위한 가상의 컴퓨터라고 할 수 있습니다.
JVM은 Java 프로그램 실행을 위해 여러 가지 중요한 역할을 수행합니다. 주요 역할은 다음과 같습니다:
이처럼 JVM은 바이트코드 실행, 메모리/자원 관리, 보안, 플랫폼 중립성 보장 등 Java 프로그램 구동에 필수적인 여러 역할을 맡고 있습니다.
Java 프로그램은 작성 -> 컴파일 -> 실행의 단계를 거쳐 JVM에서 구동됩니다. 과정을 단계별로 살펴보면 다음과 같습니다:
자바 소스 코드 작성 및 컴파일: 개발자는 .java 확장자의 소스 코드를 작성합니다. 그런 다음 Java 컴파일러(javac)를 사용하여 소스 코드를 컴파일하면, 해당 코드가 바이트코드 형태의 .class 파일로 변환됩니다. (.class 파일은 사람이 읽을 수 있는 코드는 아니며, JVM이 이해할 수 있는 이진 명령들의 모음입니다.)
클래스 로딩(Class Loading): Java 애플리케이션을 실행하면 JVM 내의 클래스 로더가 필요한 .class 파일들을 메모리에 로드합니다. 이 때 여러 클래스들이 동적으로 로딩되며, 중복 로딩을 방지하고 필요한 클래스만 로딩하도록 관리됩니다.
바이트코드 검증(Verification): JVM은 로딩된 바이트코드가 스펙을 준수하는지 검증 단계를 거칩니다. 잘못된 바이트코드나 보안에 위협이 될 수 있는 코드를 탐지하여 실행을 차단함으로써, JVM의 안정성과 보안을 확보합니다.
실행 엔진에 의한 실행: JVM의 실행 엔진(Execution Engine)이 본격적으로 바이트코드를 실행합니다. 초기에는 인터프리터가 바이트코드를 한 줄씩 읽어 해당 플랫폼의 기계어로 변환하며 실행하고, 충분한 실행 정보가 누적되면 JIT 컴파일러가 빈번히 실행되는 바이트코드 영역을 네이티브 기계어로 컴파일합니다. 이렇게 변환된 기계어 코드는 직접 하드웨어에서 실행되며, 이후에는 이미 컴파일된 코드는 재사용하여 속도를 높입니다.
실행 및 종료: 실행 엔진은 변환된 기계어 명령들을 CPU에서 수행하여 프로그램 로직을 실행합니다. 이 과정에서 JVM은 런타임 데이터 영역(Runtime Data Area)이라는 메모리 공간(메소드 영역, 힙, JVM 스택 등)을 활용하여 변수 저장, 객체 생성, 메서드 호출 등을 관리합니다. 또한 쓰레드 관리, 예외 처리 및 앞서 언급한 가비지 컬렉션 등을 수행하며 프로그램의 실행을 총괄합니다. 프로그램 실행이 끝나면 JVM은 할당했던 메모리 등을 정리하고 종료합니다.
Java 프로그램의 실행 단계: 위 다이어그램은 Java 소스 파일(.java)이 컴파일러(JDK의 javac)에 의해 바이트코드(.class 파일)로 변환되고, 그 .class 파일이 Java Runtime Environment(JRE) 내에서 실행되는 과정을 보여줍니다. JRE는 JVM과 기본 Java 클래스 라이브러리, 그리고 JIT 컴파일러 등을 포함하며, 응용 프로그램은 JRE 안에서 구동됩니다. 컴파일된 바이트코드(.class)는 어떤 컴퓨터에서도 동일한 JRE/JVM 상에서 실행되며, JVM이 해당 시스템에 맞게 바이트코드를 해석하거나 JIT 컴파일하여 최종적으로 프로그램을 동작시킵니다. 이처럼 Java는 컴파일된 코드와 실행 환경(JVM)을 분리함으로써 한 번 작성한 코드를 어디서나 실행할 수 있게 합니다.
플랫폼 독립성이란 Java 프로그램이 특정 운영체제나 하드웨어에 종속되지 않고 실행될 수 있는 특성을 말합니다. Java의 플랫폼 독립성이 가능한 이유는 JVM과 바이트코드 덕분입니다. 자바 컴파일러가 생성한 바이트코드(.class)는 특정 OS의 기계어가 아닌 중간 언어이기 때문에, 이를 직접 실행할 수 있는 것은 CPU나 OS가 아니라 각 플랫폼용 JVM입니다. Java를 실행하고자 하는 각 운영체제(윈도우, 리눅스, macOS 등)에 해당 OS에 맞는 JVM 구현체가 설치되어 있기만 하면, 동일한 .class 파일을 읽어들여 그 OS에 맞는 기계어로 변환해 실행할 수 있습니다. Sun/Oracle의 표어로 흔히 알려진 “Write Once, Run Anywhere” (한 번 작성하면 어디서든 실행) 원리가 바로 이것을 가리킵니다. 예를 들어, Windows에서 개발된 Java 프로그램이라도 .class 바이트코드 파일만 있으면 Linux나 macOS 등의 환경에서 JVM만 설치되어 있으면 수정 없이 그대로 실행할 수 있습니다. 각 플랫폼마다 JVM 구현체는 다르지만, 모두 동일한 바이트코드 명령 세트를 해석하고 실행하기 때문에 Java 프로그램의 동작 결과가 어디서나 일관되게 유지됩니다. 이처럼 JVM이 중간에서 운영체제별 차이를 흡수해주기 때문에 Java는 높은 이식성을 갖는 것입니다.
JVM은 언어 중립적인 실행 플랫폼이라고 할 수 있습니다. 그 이유는 JVM이 특정 고급 언어의 문법에 의존하지 않고 이진 바이트코드 형식만 따르기만 하면 어떤 언어로 작성된 프로그램이든 실행할 수 있기 때문입니다. 사실 JVM은 Java 언어로 만들어진 프로그램뿐만 아니라, 다른 프로그래밍 언어들도 실행할 수 있도록 설계되어 있습니다. 실제로 JVM 위에서 구동되는 언어들을 가리켜 JVM 언어라고 부르는데, Java와 문법만 다르고 바이트코드로 컴파일되어 JVM상에서 동작하는 언어들이 다수 존재합니다. 예를 들어 Kotlin, Scala, Groovy, Clojure 등은 자체적인 문법과 기능을 가졌지만 컴파일 결과가 Java와 동일한 바이트코드이므로 JVM에서 실행됩니다. 심지어 Python이나 Ruby도 JVM용 구현(Jython, JRuby)을 통해 바이트코드로 변환하여 실행할 수 있습니다. 이렇게 여러 언어를 하나의 공통된 실행 플랫폼(JVM) 위에서 구동시킬 수 있는 것은, JVM이 소스 코드의 언어적 특징보다는 바이트코드 명령의 형식과 실행 규약만을 신경 쓰도록 중립적으로 설계되었기 때문입니다. 그 결과 JVM은 자바 생태계의 범위를 넓혀 다양한 언어들의 실행 기반이 될 수 있었습니다.
Java의 플랫폼 독립성과 JVM의 역할을 간단한 예제로 확인해보겠습니다. 아래에 매우 간단한 Java 프로그램을 작성했습니다:
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JVM!");
}
}
이 소스 파일 HelloWorld.java를 컴파일하면 HelloWorld.class라는 바이트코드 파일이 생성됩니다. 이 .class 파일은 Windows이든 Linux이든 JVM이 설치된 어떤 운영체제에서나 동일한 동작을 합니다. 예를 들어, Windows에서 위 프로그램을 컴파일했다면 생성된 HelloWorld.class를 Linux 시스템으로 옮겨서 java HelloWorld 명령으로 실행할 수 있습니다. 별도의 수정이나 재컴파일 없이도 똑같이 "Hello, JVM!"라는 출력을 얻을 수 있습니다. 이는 .class 바이트코드 파일이 플랫폼 중립적이고, 각 OS에 맞는 JVM이 이를 해당 OS의 기계어로 변환해주기 때문입니다. 이처럼 JVM 덕분에 자바 프로그램은 한 번 작성하고(.java), 한 번 컴파일하면(.class), 어디서든 실행할 수 있다는 것을 직접 확인할 수 있습니다.
즉, JVM은 가상의 컴퓨터로서 바이트코드를 해석/실행하고, 메모리를 관리하며, 안전한 실행 환경을 제공하고, Java의 "Write Once, Run Anywhere" 철학을 가능케 하는 핵심 런타임 엔진입니다. 이를 통해 Java를 비롯한 다양한 언어의 프로그램들이 실제 컴퓨터에서 동작하도록 해주는 보이지 않는 기반이 바로 JVM입니다.
Java Virtual Machine(JVM)은 자바 바이트코드를 실행하기 위한 가상 컴퓨터입니다. JVM은 크게 클래스 로더 서브시스템(Class Loader Subsystem), 런타임 데이터 영역(Runtime Data Areas), 그리고 실행 엔진(Execution Engine)의 세 부분으로 구성됩니다. 각 구성 요소는 자바 프로그램을 로딩하고 메모리에 배치한 후, 바이트코드를 기계어로 해석/컴파일하여 실행하는 역할을 담당합니다. 아래 다이어그램은 JVM의 주요 구성 요소와 그 관계를 나타낸 것입니다.
JVM의 주요 구성 요소와 아키텍처: Class Loader는 .class파일을 로드하고(Linking/Initialization 포함), Runtime Data Area는 메모리 구조(메서드 영역, 힙, 스택 등)를 제공하며, Execution Engine은 바이트코드를 해석(Interpreter)하거나 JIT 컴파일하여 실행한다. 또한 JNI를 통해 네이티브 라이브러리와 연동한다.
이제 각 구성 요소별로 역할과 작동 방식을 자세히 살펴보겠습니다. 또한 클래스 로더의 로딩 단계, 메모리 영역의 세부 구조, 실행 엔진의 동작(인터프리터 vs JIT 컴파일러)과 JVM 전체 흐름을 예제와 함께 설명하겠습니다.
클래스 로더(Class Loader)는 JVM이 동적으로 클래스들을 로드하는 역할을 담당하는 구성 요소입니다. 자바는 “런타임에 필요한 클래스만 그때그때 메모리에 적재”하는 동적 클래스 로딩을 특징으로 하며, 이는 클래스 로더 서브시스템에 의해 구현됩니다. 자바 컴파일러(javac)가 생성한 .class 파일(바이트코드)은 JVM 실행 시점에 클래스 로더에 의해 읽혀져 메모리에 로딩됩니다.
역할 및 기능: 클래스 로더는 특정 클래스가 처음 참조될 때(사용될 때) 해당 클래스의 바이트코드를 찾아 메모리(Method Area)에 적재하고, 링크(Link)와 초기화(Initialization)를 수행합니다. 이 과정에서 동적 로딩뿐 아니라 격리와 네임스페이스 관리 기능도 제공합니다. 서로 다른 클래스 로더는 동일한 이름의 클래스라도 별도로 로딩하여 독립적인 네임스페이스를 갖게 할 수 있습니다. 또한 한 번 로드된 클래스는 언로드(unload)되지 않으며(JVM 종료 전까지 메모리에 유지)
, 클래스를 언로드하려면 해당 클래스 로더 자체를 폐기해야 합니다.
클래스 로더의 종류와 계층: JVM에는 기본적으로 세 종류의 클래스 로더가 계층적으로 동작합니다 (Java 8 기준):
부트스트랩 클래스 로더(Bootstrap ClassLoader) – 최상위 부모 클래스로더로, JVM 구동 시 가장 먼저 실행됩니다. JRE의 핵심 클래스들을 로드하며, 예를 들어 <JAVA_HOME>/jre/lib/rt.jar 안의 자바 표준 라이브러리 클래스들이 여기서 로딩됩니다. 이 로더는 자바 코드가 아니라 네이티브 코드(C++)로 구현되어 있다는 특징이 있습니다.
익스텐션 클래스 로더(Extension ClassLoader) – 부트스트랩의 자식 로더로, JDK 8까지는 <JAVA_HOME>/lib/ext/ 디렉토리의 확장 라이브러리(JAR)들을 로딩합니다. (Java 9부터는 Platform ClassLoader로 대체됨) 주로 보안 확장 기능 등의 클래스를 로드하며, 부트스트랩 로더와 애플리케이션 로더 사이에서 동작합니다.
애플리케이션 클래스 로더(Application ClassLoader) – 익스텐션 로더의 자식 로더로, 일반 자바 애플리케이션의 클래스 경로(CLASSPATH)에 있는 클래스들을 로딩합니다. 예컨대 애플리케이션의 .jar나 bin 디렉토리에 있는 사용자 코드들이 이 로더를 통해 메모리에 올라갑니다. 이것을 시스템 클래스 로더(System ClassLoader)라고도 부르며, 우리가 별도 로더를 만들지 않으면 기본적으로 이 로더가 애플리케이션 클래스를 읽어들입니다.
(+) 사용자 정의 클래스 로더(User-Defined ClassLoader): 위 계층 구조 밖에서, 애플리케이션 클래스 로더 이후에 동작할 수 있도록 사용자가 직접 작성하는 커스텀 클래스 로더도 존재합니다. 이는 특수한 클래스 로딩 동작(예: 네트워크 통해 클래스 로딩 등)이 필요할 때 활용됩니다.
위임 메커니즘 (Delegation): 클래스 로더들은 위임(delegation) 기반의 계층적 모델로 동작합니다. 즉, 어떤 클래스 로더가 클래스를 로딩할 때 먼저 부모 로더에 로딩을 위임하고, 부모가 해당 클래스를 찾지 못한 경우에만 자신이 로딩을 수행합니다. 이러한 위임 계층 알고리즘(Delegation Hierarchy Algorithm) 덕분에, 기본 Java 라이브러리의 클래스들은 언제나 부트스트랩 로더가 우선 로딩하게 되어 일관성을 유지하고 중복 로딩을 피할 수 있습니다. 예를 들어, 애플리케이션 로더가 java.lang.String 클래스를 로딩하려 하면 먼저 부모인 부트스트랩 로더에게 위임하고, 부트스트랩 로더가 이미 String 클래스를 로드했으므로 재사용하는 식입니다. 반대로, 부모 로더가 로딩하지 못한 클래스를 자식이 대신 로드할 수는 있지만, 부모가 이미 로딩한 클래스는 자식이 다시 로드할 수 없습니다. 이런 규칙을 가시성 규약(Visibility Constraint)이라고도 하며, 하위 로더는 상위 로더의 클래스를 볼 수 있지만 그 반대는 불가능합니다.
클래스 로더는 클래스 로딩 과정에서 세 단계를 수행합니다:
로딩 (Loading): .class 파일을 찾아서 바이트코드를 읽어들이고, JVM 메모리(Method Area)에 해당 클래스의 내용을 로드합니다. 이 때 앞서 언급한 Bootstrap → Extension → Application 클래스로더 순으로 위임하여 클래스를 찾습니다. 가장 우선순위 높은 부트스트랩 로더부터 탐색하고, 해당 클래스가 발견되면 로딩을 완료하며 단계를 마칩니다.
링크 (Linking): 로드된 클래스가 실행되기 전에 검증 및 준비 작업을 수행합니다. 링크 단계는 다시 세 가지로 나뉩니다:
검증 (Verification): 메모리에 로드된 바이트코드가 JVM 명세에 맞는 유효한 코드인지 검사합니다. 바이트코드 검증기를 통해 잘못된 바이트코드나 보안에 위배되는 코드가 없는지 확인하며, 검증 실패 시 VerifyError가 발생합니다.
준비 (Preparation): 로드된 클래스의 메타데이터를 초기화하는 단계입니다. 주로 클래스에 선언된 정적 변수(static fields)를 위한 메모리를 할당하고 기본값으로 초기화합니다. (예를 들어 static int x = 10; 이라면 이 단계에서는 x의 메모리를 확보하고 타입의 기본값인 0으로 세팅해둡니다. 실제 10으로 설정하는 것은 초기화 단계에서 수행됨)
해결 (Resolution): 클래스 내부에서 참조하는 다른 클래스, 메소드, 필드 등의 심볼릭 참조(symbolic reference)를 실제 메모리 주소 등의 직접 참조로 치환합니다. 예를 들어 이 클래스가 java.lang.String을 참조하고 있다면, 심볼 형태("java/lang/String")로 되어있는 것을 이미 로드된 String 클래스 객체를 가리키는 레퍼런스로 변경합니다. 이 과정으로 이후 실행 시 빠르게 해당 참조들을 사용할 수 있게 됩니다.
JVM 클래스 로딩의 전체 흐름이며, 클래스 로더는 이처럼 로딩 → 링크 → 초기화 순서로 동작합니다. 이러한 동적 클래스 로딩 방식 덕분에, 자바는 필요할 때까지 클래스를 로드하지 않으므로 메모리 효율을 높이고 (사용하지 않는 클래스는 로드되지 않음), 동적 확장(런타임에 새로운 클래스 로딩)도 가능하게 합니다.
| 예시: 간단한 자바 프로그램을 예로 클래스 로더의 역할을 보겠습니다. 아래는 Test라는 클래스의 일부입니다:
public class Test {
static int s = 42; // 정적 변수
public static void main(String[] args) {
Test obj = new Test(); // Test 클래스 로드 및 객체 생성
int result = obj.calc(5);
System.out.println(result);
}
public int calc(int x) {
int y = 2;
return x + y;
}
}
이 프로그램을 컴파일하면 Test.class가 생성되고, 실행 시 JVM은 먼저 Test 클래스를 Application ClassLoader로 로드합니다. 로딩 단계에서 Test.class 파일이 읽혀 메모리에 올라가고, 링크의 검증을 통해 바이트코드를 확인한 뒤, 준비 과정에서 정적 변수 s를 위한 메모리를 확보하여 기본값 0을 넣어둡니다. 해결 단계에서는 Test 클래스가 참조하는 java.lang.Object 등의 심볼릭 참조를 실제 클래스 객체로 연결합니다. 이어서 초기화 단계에서 s에 42를 대입합니다 (또 클래스 초기화 블록이 있었다면 실행). 이로써 Test 클래스 로딩이 완료되어 사용할 준비가 되었습니다.
그런 다음 JVM은 main 메서드를 찾아 실행을 시작합니다. 이 때 런타임 데이터 영역에 본격적으로 스레드 스택과 힙 등이 활용되는데, 이에 대해서는 아래에서 설명하겠습니다.클래스 로더는 이후에도 Test 클래스 실행 도중 다른 클래스 (예: System 클래스 또는 사용자 정의 다른 클래스)를 처음 참조할 때마다 같은 방식으로 해당 클래스를 동적으로 로드하고 링크/초기화를 수행합니다.
런타임 데이터 영역은 JVM이 프로그램 실행을 위해 사용하는 메모리 영역들을 가리킵니다. JVM이 실행되면 운영체제로부터 할당받은 메모리를 논리적으로 여러 영역으로 나누어 관리하는데, Java 명세에 따르면 5가지 주요 영역으로 구성됩니다:
메서드 영역 (Method Area) – “메타스페이스(Metaspace)”라고도 불리며, 클래스 수준의 정보를 저장하는 영역입니다. JVM당 한 개만 존재하며, 모든 스레드가 공유합니다. 클래스 로더가 적재한 클래스의 바이트코드가 이곳에 저장되고, 클래스별로 런타임 상수 풀(Runtime Constant Pool), 필드와 메서드 정보, 메서드의 바이트코드 등이 포함됩니다. 또한 정적 변수(static fields)와 정적 메서드도 메서드 영역에 저장됩니다. (Java 8부터는 PermGen이 폐지되고 메타스페이스로 구현되었지만, 논리적인 역할은 Method Area와 같습니다.)
힙 영역 (Heap Area) – 객체를 저장하는 영역입니다. new 연산으로 생성된 모든 인스턴스 객체와 배열은 힙에 할당됩니다. 힙도 메서드 영역처럼 JVM 전체에 하나만 존재하며 스레드 모두가 공유하는 공간입니다.
자바 스택 영역 (Java Stack Area) – 각 스레드마다 개별적으로 할당되는 스택으로, 메서드 호출 시 생성되는 스택 프레임(Stack Frame)을 저장합니다.
PC 레지스터 (Program Counter Register) – 각 스레드마다 하나씩 존재하는 레지스터로, 현재 실행 중인 JVM 명령의 주소(위치)를 가리키는 값을 보관합니다. CPU의 프로그램 카운터와 유사한 역할을 하지만, JVM 명령어의 주소를 가집니다.
네이티브 메서드 스택 (Native Method Stack) – 자바가 아닌 네이티브 언어(C/C++ 등)로 작성된 코드를 위한 스택입니다. 각 스레드마다 하나씩 존재하며, 자바에서 native 키워드가 붙은 메서드를 호출할 때 해당 네이티브 코드 실행을 위한 스택 프레임을 제공합니다.
이상 5가지 영역이 JVM의 런타임 데이터 영역을 이루며, JVM이 시작될 때 메서드 영역과 힙이 생성되고, 각 스레드가 생성될 때마다 그 스레드 전용의 JVM 스택, PC 레지스터, 네이티브 스택이 생성됩니다.
메서드 영역과 힙처럼 공유되는 영역은 JVM 전역에서 하나만 존재하고, 스레드 전용 영역(스택, PC, 네이티브 스택)은 스레드마다 별개로 존재합니다.
프로그램 실행 중에는 클래스 로더를 통해 새 클래스와 객체가 메모리에 추가되고, 실행이 진행되며 스택 프레임이 추가/제거되고, PC 레지스터가 변경되는 등의 일이 끊임없이 일어납니다. 실행이 끝나면 사용한 메모리 영역들은 정리되며 JVM이 종료됩니다.
예시 (스택과 힙의 동작): 앞서 예로 든 Test 프로그램의 실행 흐름을 메모리 관점에서 살펴보겠습니다. 클래스 로더에 의해 Test 클래스가 메서드 영역에 로드되고 나면, JVM은 main 메서드를 호출하여 실행을 시작합니다. 이때 메인 스레드가 시작되며, 메인 스레드 전용 스택이 생성되고 PC 레지스터가 main 메서드의 첫 바이트코드 명령을 가리키도록 설정됩니다.
실행 엔진은 메모리에 로드된 바이트코드를 실제 CPU가 실행할 수 있는 기계어로 변환하여 실행하는 JVM 구성 요소입니다. 클래스 로더가 적재한 바이트코드는 이 실행 엔진에 의해 한 명령어씩 처리됩니다. 실행 엔진은 크게 인터프리터(Interpreter)와 JIT(Just-In-Time) 컴파일러, 그리고 가비지 컬렉터(Garbage Collector) 등을 포함합니다.
인터프리터 (Interpreter): 인터프리터는 바이트코드를 한 줄 한 줄 읽어서 즉시 실행하는 방식으로 동작합니다. 바이트코드 명령을 한 개 가져와서 해석하고 해당하는 기계 동작을 수행한 뒤, 다시 다음 명령을 가져오는 식입니다.
JIT 컴파일러 (Just-In-Time Compiler): JIT 컴파일러는 이러한 인터프리터의 단점을 보완하기 위해 JVM에 탑재된 동적 컴파일러입니다. 인터프리터와 함께 동작하면서, **반복 실행되는 바이트코드 조각을 기계어로 한번에 컴파일하여 변환합니다. 이렇게 컴파일된 네이티브 코드를 캐시해두고 이후에는 재사용하므로, 같은 바이트코드를 다시 해석할 필요 없이 바로 기계어로 실행하게 되어 속도가 크게 향상됩니다.
가비지 컬렉터 (Garbage Collector): 실행 엔진의 또 다른 핵심 구성 요소로, 힙 메모리를 관리하는 역할을 합니다. GC는 주기적으로 힙을 스캔하여 사용되지 않는 객체를 탐지하고 메모리를 해제함으로써, 메모리 누수를 방지하고 프로그램이 장기간 실행되어도 메모리가 고갈되지 않도록 합니다. Java의 GC는 Mark-and-Sweep, Generational GC 등 다양한 알고리즘으로 진화해왔으며, JVM 옵션으로 GC 방식을 선택할 수도 있습니다. (예: Serial, Parallel, G1, ZGC 등)
JNI (Java Native Interface)와 네이티브 메서드 지원: JVM 실행 엔진은 또한 JNI를 통해 네이티브 라이브러리와 연동할 수 있습니다. 즉, 자바 코드에서 C/C++ 등의 네이티브 함수를 호출하거나 반대로 호출될 수 있습니다. 실행 엔진은 네이티브 메서드를 호출하면 해당 라이브러리를 로드하고, 네이티브 스택을 활용하여 외부 코드 실행을 관리합니다. 이때 JNI는 자바 객체를 네이티브 코드에서 다룰 수 있도록 포인터를 제공하거나, 반대로 네이티브 데이터를 자바 쪽으로 변환해주는 등의 역할을 합니다.
정리하면, 실행 엔진은 로드된 바이트코드를 가져와 실제 실행을 담당하며, 인터프리터로 즉각적 실행을, JIT 컴파일러로 반복 코드의 최적화를, GC로 메모리 관리를 수행함으로써 JVM의 핵심 기능인 "한 번 작성한 코드(.class)를 어디서나 효율적으로 실행"하는 작업을 구현합니다.
지금까지 각 부분별로 JVM 내부 동작을 살펴보았는데, 이것들을 종합하여 JVM이 자바 코드를 실행하는 흐름을 단계별로 정리해 보겠습니다:
위의 흐름에서 볼 수 있듯이, JVM의 각 구성 요소들은 긴밀하게 상호작용합니다. 클래스 로더가 코드와 데이터를 메모리에 올리면, 런타임 데이터 영역이 저장소가 되고, 실행 엔진이 그것을 이용하여 실행합니다. 실행 중에는 다시 클래스 로더를 호출하기도 하고, 메모리 영역에 변화를 주기도 합니다. 이러한 구조 덕분에 Java는 한 번 컴파일된 바이트코드를 어디서나 실행할 수 있고 (JVM이 추상화 계층 역할), 동시에 JIT 컴파일 등으로 높은 성능도 확보할 수 있습니다.
요약하면, JVM은 “클래스 로더 → 메모리 적재 → 바이트코드 실행(인터프리터/JIT) → 메모리 관리”의 사이클로 동작하며, 각각의 구성 요소가 맡은 역할을 수행함으로써 Java 프로그램이 안전하고 효율적으로 실행됩니다. 이러한 JVM 아키텍처를 잘 이해하면 Java의 메모리 관리나 성능 튜닝, 클래스 로딩 이슈 등을 더 깊이 있게 다룰 수 있게 됩니다.