Java Virtual Machine (JVM)은 OS에 상관없이 CPU가 자바를 실행할 수 있게 해주는 가상 컴퓨터이다.
.java 확장자를 가진 소스 코드는 CPU가 직접 이해할 수 없는 형태이기 때문에, 실행 전에 기계어로의 변환 과정이 필요하다.
그러나 Java는 다른 언어와 달리, 소스 코드를 바로 기계어로 변환하지 않고 JVM(자바 가상 머신) 위에서 실행되기 때문에, 먼저 JVM이 이해할 수 있는 바이트코드(.class)로 변환된다.
이 컴파일 과정은 JDK에 포함된 javac.exe가 수행하며, JDK를 설치하면 /bin 디렉토리 내에서 확인할 수 있다.
컴파일된 바이트코드는 OS에서 직접 실행되지 않고, JVM이 이를 해석하거나 JIT 컴파일을 통해 기계어로 변환하여 실행한다.
이러한 구조 덕분에 바이트코드는 운영체제에 종속되지 않고, JVM만 설치되어 있다면 어떤 플랫폼에서든 동일한 .class 파일을 실행할 수 있다.
즉, Java는 "Write Once, Run Anywhere"라는 철학을 바탕으로, 하나의 소스 코드로 다양한 환경에서 호환 가능한 실행을 지원한다.
자바 바이트코드(Java Bytecode)는 자바 소스코드를 컴파일한 결과물로, JVM이 이해할 수 있는 중간 언어를 의미한다.
이 바이트코드는 JVM에 의해 실행되며, 실행 시에는 인터프리터(Interpreter) 또는 JIT(Just-In-Time) 컴파일러에 의해 기계어(바이너리 코드)로 변환되어 실제 CPU에서 실행된다.
JIT(Just-In-Time) 컴파일러, 또는 동적 번역기는 프로그램을 실행하는 시점에 바이트코드를 기계어로 변환하는 컴파일러다.
JIT 컴파일러는 컴파일된 기계어 코드를 캐시에 저장하므로, 한 번 컴파일된 코드는 이후에 매우 빠르게 실행된다.
다만, 바이트코드를 JIT로 컴파일하는 작업은 인터프리팅보다 시간이 더 오래 걸리기 때문에, 한 번만 실행되는 코드라면 굳이 컴파일하지 않고 인터프리터 방식으로 처리하는 것이 효율적이다.
이러한 이유로 JIT을 사용하는 JVM은 내부적으로 메서드나 루프의 실행 빈도를 추적하고, 일정 기준(Threshold) 이상 반복 실행되는 경우에만 해당 코드를 JIT 컴파일한다. (이를 흔히 핫스팟(Hot Spot)이라고 부른다)
즉, 자바 프로그램은 먼저 자바 컴파일러(javac)에 의해 바이트코드로 변환되고, 이 바이트코드는 JVM에 의해 실행 시점에 인터프리트되거나, JIT을 통해 기계어로 최적화되어 실행된다.

JVM은 컴파일된 .class 파일을 메모리에 로드하고, 링크를 통해 연결 및 배치하는 작업을 수행한다.
런타임 중에는 필요한 클래스를 동적으로 로딩하며, JAR 파일에 포함된 클래스들도 JVM 위에 탑재되어 실행된다. JVM 클래스 로딩 과정은 총 3단계로 이루어져 있다.
1. 로드(Load)
클래스 로딩의 첫 단계는 .class 파일을 JVM 메모리 영역(Method Area)에 읽어들이는 것이다.
이때 클래스 이름, 메서드, 필드 정보, 상수 풀(Constant Pool) 등이 JVM 내부에 등록된다.
JVM은 클래스 이름을 기준으로 ClassLoader를 통해 .class 파일을 찾아 로드하며, 이때 JAR 파일 안에 있는 클래스도 함께 읽어올 수 있다.
2. 링크(Link)
링크 단계는 로드된 클래스의 유효성을 검사하고, 실행에 필요한 메모리 구조를 준비하며, 클래스 간의 참조를 연결하는 작업을 수행한다. 총 3단계로 구성된다.
검증(Verification)
바이트코드가 JVM 명세에 맞게 작성되었는지를 확인한다.
예를 들어, 접근 제한을 위반하지 않았는지, 명령어가 올바른지 등을 검증한다.
준비(Preparation)
클래스의 static 필드에 기본값(0, null 등) 을 할당한다.
예를 들어, static int count = 10;이라는 코드가 있다면, 이 단계에서는 count에 0만 저장된다.
해석(Resolution)
상수 풀(Constant Pool)에 있는 심볼릭 참조들을 실제 메모리 주소로 변환한다.
즉, 메서드 호출이나 클래스 참조를 실행 가능한 형태로 연결하는 과정이다.
3. 초기화(Initialization)
초기화는 클래스 로딩의 마지막 단계로, 실제 실행 가능한 상태로 만드는 과정이다.
이로써 클래스는 모든 초기 준비를 마치고 실행할 준비가 완료된다.
이 단계에서는 다음 작업들이 수행된다:
static 변수에 코드에서 정의한 값을 할당 (count = 10;)
static 초기화 블록 (static { ... }) 실행
실행 엔진은 JVM이 자바 바이트코드(.class)를 실제 실행하는 역할을 수행한다.
자바 바이트코드는 사람이 이해하기 쉬운 중간 언어로, CPU가 직접 실행할 수 있는 기계어와는 다르다.
따라서 실행 엔진은 바이트코드를 JVM 내부에서 기계가 실행 가능한 형태로 변환하고 수행한다.
인터프리터
실행 엔진은 바이트코드를 명령어 단위로 해석하며 한 줄씩 실행한다. 실행 시작 속도가 빠르다는 장점이 있지만, 전체 실행 속도는 상대적으로 느릴 수 있다.
JIT
초기에는 인터프리터 방식으로 실행하지만, 자주 실행되는 코드(핫스팟)를 감지하면
해당 바이트코드를 기계어로 통째로 컴파일하여 성능을 향상시킨다.
이후부터는 해당 코드를 인터프리팅 없이 기계어로 직접 실행한다.
Garbage Collector
JVM은 실행 중 더 이상 사용되지 않는 객체(인스턴스)를 감지하고,
이를 메모리에서 자동으로 회수하여 메모리 누수를 방지하고 자원을 효율적으로 관리한다.
Runtime Data Area는 자바 코드를 실행하기 위해 JVM이 사용하는 메모리 영역들을 통칭하는 용어이다.
이 영역들은 클래스 정보, 객체, 지역 변수, 실행 위치 정보 등 프로그램 실행에 필요한 다양한 데이터를 저장하며, 각각의 역할에 따라 구분된다.

PC Register
PC Register는 JVM이 실행 중인 바이트코드 명령어의 주소 또는 위치를 저장하여,
다음에 어떤 명령어를 실행할지 추적할 수 있도록 돕는 스레드별 메모리 공간이다.
JVM 스택 영역
메서드 호출 시 필요한 데이터(지역 변수, 매개변수, 연산 결과 등)를 저장하는 스레드별 메모리 공간이다.
JVM에서 자바 프로그램을 실행할 때, 메서드가 호출되면 Stack Frame(스택 프레임)이라는 단위로 메모리에 쌓이고, 메서드 실행이 끝나면 해당 프레임은 제거된다(=pop).
Stack Frame 구조는 다음과 같다.
Local Variable Array (지역 변수 배열)
메서드에 사용되는 모든 지역 변수와 매개변수가 저장됨
Operand Stack (피연산자 스택)
연산 수행을 위한 임시 공간 (덧셈, 곱셈 같은 계산 중간값 저장)
Frame Data (리턴 주소, 예외 핸들링 등)
JVM 내부에서 관리용으로 사용됨
Native method stack
자바는 플랫폼 독립적이지만 현실적으로 OS에 접근하거나 성능을 위해 C/C++ 코드와 연동하는 경우가 있다. 이때 사용하는 것이 JNI(Java Native Interface)이고, JNI를 통해 호출되는 native 메서드가 실행될 때 필요한 데이터를 저장하는 공간이 Native method stack이다.
Method Area(Static area)
클래스 정보를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.
멤버 변수 정보, 메소드 정보, 클래스 타입등이 올라간다.
interface 또는 object는 Heap에서 관리한다.
Runtime Constant Pool
클래스나 인터페이스에서 사용하는 상수 및 심볼릭 참조 정보를 저장하는 공간으로,
중복을 방지하고 효율적인 상수 관리를 위해 설계된 JVM의 메모리 영역이다.
Heap
객체를 저장하는 가상메모리 공간. new 연산자로 생성되는 객체와 배열을 저장한다.
Class Area(Static Area)에 올라온 클래스들만 객체로 생성할 수 있다.