java를 조금씩 깊게 공부를 하다보니 JVM의 역할과 구조를 정확하게 정리하는 단계가 필요하다고 느껴졌다.
학교 수업을 통해 대충 알기는 했지만 완전히 빠삭하게 내용을 숙지한 것 같지는 않아 이번 기회에 확실하게 잡고가겠다.
출처: https://hstory0208.tistory.com/entry/Java-JVM이란-구조와-특징에-대해-알아보자 [< Hyun / Log >:티스토리]

우리가 java 파일을 실행하려면 위와같은 과정을 거쳐야한다.
우선 java compiler로 java코드를 bytecode(.class file)로 변환한다.
변환하는 이유는 java파일을 윈도우, 맥, 리눅스 등 다양한 운영체제에서 돌아갈 수 있게하기 위해서이다.
이후 bytecode를 원하는 운영체제에 맞는 JVM을 이용하여 해당 운영체제가 이해할 수 있는 기계어로 변환한다.
추가로 c/c++의 경우에는 컴파일러가 기계어 코드로 변환해준다.
JVM의 구조는 다음과 같다.

GC는 힙 메모리 영역에 생성된 객체들 중 더 이상 참조되지 않는 객체를 자동으로 검색해 제거한다.
참고로 GC는 지금 객체가 계속 사용중인지 아닌지 체크도 하고 삭제도 해야하기 때문에 GC를 시작하기 전에 JVM내부의 모든 쓰레드를 멈춰 메모리 상태를 고정해두고 작업을 한다. 이후 작업이 끝난 뒤 쓰레드를 다시 실행.
이 때 쓰레드를 멈추기 때문에 프로그램이 순간적으로 멈추는데 이것을 Stop-The-World라고 한다.
아무래도 프로그램이 멈추는 것은 꽤나 중요한 사태(?)이기 때문에 이를 조절할 GC 튜닝이나 알고리즘 선택이 중요하다. (G1 GC, Parallel GC, CMS) 해당 내용은 추후에 제대로 정리할 계획이다.
메모리(Method Area)에 올려진 바이트코드를 기계가 이해하는 형태로 변환하고 실행하는 역할을 수행한다.
Execution Engine 세부 역할로는
Interpreter
바이트코드(.class)를 한 줄씩 읽어서 실행.
같은 코드를 실행할 때마다 바이크코드를 매번 해석해야해 속도가 느리다는 단점이 있다.
JIT 컴파일러 (Just-In-Time Compiler)
실행 중에 필요한 부분만 바이트코드를 기계어로 바꿔준다.
인터프리터의 단점을 보완하기 위해 도입된 것으로 프로그램 실행 중에 바이트코드 전체 또는 일부를 네이티브 코드(CPU가 바로 실행할 수 있는 이진 코드)로 컴파일하고, 직접 실행한다.
초기 컴파일에는 시간이 걸리지만, 한 번 컴파일된 코드는 매우 빠르게 실행된다.
또한 JIT 컴파일러는 자주 실행되는 코드(핫스팟)를 분석해 우선적으로 컴파일하여 성능을 최적화한다.
동작 흐름을 요약하자면
.class 로딩 → 바이트코드 → 인터프리터 실행 시작
↓
반복 호출되는 코드 발견 (핫스팟)
↓
JIT 컴파일러가 Native Code 변환
↓
해당 부분 초고속 실행
.class 파일을 JVM으로 가져와서 메모리(Method Area)에 클래스의 메타정보(클래스 이름, 상속정보, 메서드 목록, 필드 정보)를 올리는 역할을 하는 시스템이다.
메모리에 올린 .class를 Execution Engine이 실행한다.
추가로 class loader는 필요할 때 동적으로 클레스를 메모리에 올리는 동적로딩을 담당한다.
자바 프로그램 실행 중 JVM이 사용하는 메모리 영역 전체를 통틀어 Runtime Data Area라고 한다.
Runtime Data Area는 RAM에 포함되어 있다.
Runtime Data Area는 다음과 같이 이루어져 있다.
여기서 Method Area와 Heap은 JVM 전체가 공유하는 공유 영역이고
Stack, PC Register, Native Method Stack은 각각의 쓰레드 별로 존재하는 영역이다.

이제 Runtime Data Area의 구성요소를 하나씩 알아보자
클래스 수준의 메타데이터 저장 영역으로 모든 스레드가 사용하는 공유 영역이다.
Method Area에 저장되는 내용은 다음과 같다
| 저장되는 내용 | 설명 |
|---|---|
| 클래스 이름, 패키지명 | 클래스의 완전한 이름 및 소속 패키지 |
| 상속 정보 (부모 클래스, 인터페이스) | 클래스의 상속 관계, 구현하는 인터페이스 목록 |
| 필드 정보 (멤버 변수) | 인스턴스 변수와 static 변수의 타입 및 이름 |
| 메서드 정보 | 메서드의 시그니처(이름, 매개변수, 반환타입 등) |
| static 변수 | 클래스 차원의 변수, 객체와 무관하게 Method Area에 저장 |
| constant pool (상수 풀) | 리터럴, 상수, 심볼릭 참조(클래스명, 메서드명 등) 저장 |
| final 변수 (컴파일 상수화된 값) | 변하지 않는 상수 값 저장 |
| static 초기화 블록, static 메서드 구현부 | 클래스 초기화 시 실행될 static 블록과 static 메서드 코드 |
Method Area에 클래스가 로드되는 과정은 다음과 같다.
1. Loading
.class 파일을 읽어옴
2. Linking
Verification: 바이트코드 검증
Preparation: static 변수 메모리 할당 (기본값으로)
Resolution: 심볼릭 참조 → 실제 주소 변환
3. Initialization
static 변수 값 초기화
static 블록 실행
참고로 이 모든 정보는 Method Area에 저장된다.
public class Example {
static int x = 10;
int y = 20;
public void print() {
System.out.println("Hello");
}
}
클래스 이름: Example
상속 정보: Object를 상속
static 변수: x = 10
인스턴스 변수 정보: y
메서드 정보: print() 시그니처
constant pool: "Hello" 문자열 리터럴, 메서드 참조 심볼 등
JVM의 Method Area를 구현를 구현하는 방식으로 Metaspace와 PermGen이 있는데 PermGen은 Java8 이전, Metaspace는 Java8 이상의 버전에서 적용된다.
간략하게 설명하자면 PermGen은 JVM 내부 힙 메모리를 사용하고 크기가 고정되어있어서 크기가 부족할시 OOM(OutOfMemoryError)가 발생한다.
반면 Metaspace는 네이티브 메모리 (OS 메모리) 사용하고 크기를 유동적으로 조절이 가능하여 OOM이 발생할 확률이 적다.
heap은 프로그램 실행 중 생성되는 객체 인스턴스 저장 공간으로 new 키워드 또는 동적 객체 생성 시 메모리가 이 영역에 할당된다.
JVM의 Runtime Data Area 중 가장 큰 영역을 차지하고 Garbage Collection(GC)의 대상이 된다.

JVM의 Heap은 위와 같이 Young Generation(Eden + S0 + S1)과 Old Genertaion(Tenured) 으로로 이루어져 있다.
Young Generation은 생성된지 얼마 안된 객체 저장되는 장소로 Eden space와 Survivor Space(s0, s1)로 이루어져 있으며
Old Generation은 상대적으로 나이가 많은(?) 객체들이 저장되어있는 장소이다.
이 구조는 GC(Garbage Collection)와 밀접한 관련이 있는데
GC가 발생하지 않은 새로 생성된 객체는 Eden에 저장된다.Eden이 가득차면 발생하는 Minor GC에서 살아남은 객체는 S0 또는 S1으로 이동한다. S0과 S1에 있는 객체는 Minor GC가 발생할 때마다 S0과 S1을 교차하면서 이동한다.Minor GC에서 일정 횟수 이상 살아남은 객체들은 마지막으로 Old Generation으로 승격(?)하게 된다. 이곳에서는 Major GC가 발생하고 상대적으로 GC의 빈도수가 낮다. 이와같은 과정으로 객체는 JVM Heap에 저장된다.
JVM의 Stack은 각 쓰레드마다 하나씩 존재하는 메모리 영역으로, 자바 프로그램 실행 시 메서드 호출에 필요한 정보를 저장하는 공간이다.
스택은 메서드 호출과 종료에 따라 프레임(Frame) 단위로 push/pop 되며, 함수 호출 스택(호출 스택) 역할을 한다.
| 항목 | 설명 |
|---|---|
| 단위 | Stack Frame (메서드 호출 1회당 1개 생성) |
| 저장 정보 | 로컬 변수, 피연산자 스택, 메서드 정보, 예외 핸들링 정보 등 |
| 생성 시점 | 스레드가 시작될 때 생성됨 |
| 소멸 시점 | 스레드가 종료될 때 소멸됨 |
| 접근 방식 | LIFO (Last In First Out) |
| GC 대상 여부 | ❌ (GC가 관리하지 않음. 자동 소멸) |
| 오류 종류 | StackOverflowError, OutOfMemoryError |
메서드가 호출되면 JVM Stack에 Stack Frame이 하나 push되고, 메서드가 끝나면 pop된다.
각 Stack Frame은 다음과 같이 구성된다:
Local Variable Array (로컬 변수 배열)
메서드의 매개변수와 지역변수가 저장됨
int, float, 참조형 등 포함됨
Operand Stack (피연산자 스택)
명령어가 실행될 때 사용되는 피연산자 저장소
예: iadd 연산은 피연산자 스택의 두 정수를 꺼내 덧셈 후 다시 push
Frame Data (프레임 정보)
상위 호출자와의 연결 정보
예외 처리 테이블
리턴 주소 등
public class Example {
public static void main(String[] args) {
int result = add(2, 3);
}
public static int add(int a, int b) {
return a + b;
}
}
main()이 호출되면 Stack Frame 생성 → JVM Stack에 push
add(2, 3) 호출되면 새로운 Stack Frame 생성 → push
a와 b는 Local Variable Array에 저장
a + b 연산은 Operand Stack에서 수행
결과 return → add()의 Frame pop → main()으로 돌아감
프로그램 종료 → JVM Stack 사라짐
StackOverflowError
무한 재귀 호출 시 발생
public void infinite() {
infinite(); // 계속 Stack Frame이 쌓이다가 터짐
}
OutOfMemoryError: StackOverflowError
전체 메모리가 부족하여 스택 생성조차 못할 때 발생
JVM의 PC Register는 각 스레드가 실행 중인 명령어의 주소(위치)를 저장하는 레지스터이다.
자바 바이트코드에서 어떤 명령어를 현재 실행 중인지, 그리고 다음에 무엇을 실행할 것인지를 결정하는 데 사용된다.
| 항목 | 설명 |
|---|---|
| 역할 | 현재 실행 중인 JVM 명령어의 주소를 저장 |
| 단위 | 바이트코드 명령어의 주소(오프셋) |
| 스레드별 존재 | ✅ 스레드마다 별도로 존재 (Thread-private) |
| 메모리 크기 | 매우 작음 (주소값 하나만 저장하므로) |
| GC 대상 여부 | ❌ GC 관리 대상 아님 |
| 예외 발생 시 동작 | 예외 처리를 위해 적절한 명령어 위치로 PC 레지스터가 이동 |
JVM은 자바 코드를 클래스 파일로 컴파일한 후, 바이트코드 명령어를 실행하는데, 이때 PC Register는 이 바이트코드의 현재 위치를 기억하는 역할을 한다.
| 구분 | JVM의 PC Register | C언어에서의 레지스터 (예: eax, ecx, rip, etc.) |
|---|---|---|
| 속한 대상 | JVM (Java Virtual Machine) | 실제 CPU 하드웨어 |
| 용도 | 현재 실행 중인 바이트코드의 주소(위치) 추적 | 명령어 실행, 데이터 저장, 주소 계산 등 다양한 역할 |
| 사용 방식 | 바이트코드 해석을 위한 내부 주소 추적용 | 기계어 명령을 실행하기 위한 레지스터 수준 연산 |
| 크기 | 매우 작고 JVM 내부에서만 사용 | 보통 32비트/64비트 하드웨어 레지스터 |
| 예시 | JVM 내부에서 iload, iadd 등의 명령 위치 추적 | RIP, EAX, RCX, ESP 등 CPU의 물리 레지스터 |
자바 코드에서 네이티브(native) 메서드를 호출할 때 사용하는 스택이다.
자바에서는 JNI (Java Native Interface)를 통해 C/C++ 같은 네이티브 언어로 구현된 메서드를 호출할 수 있는데, 이때 JVM이 바이트코드가 아닌 기계어(native code)를 실행해야 하므로, 자바 스택이 아닌 별도의 스택이 필요한데 그게 바로 Native Method Stack이다.
public class Example {
public native void nativeMethod(); // C/C++로 구현됨
static {
System.loadLibrary("nativeLib"); // 네이티브 라이브러리 로드
}
}
| 구분 | JVM Stack | Native Method Stack |
|---|---|---|
| 실행 대상 | 자바 메서드 (바이트코드) | 네이티브 메서드 (C, C++ 등) |
| 언어 | 자바 | C, C++ (JNI) |
| 메모리 구조 | JVM에 의해 관리되는 스택 프레임 구조 | 운영체제에 따라 다름 (일반적인 C 함수 스택 구조) |
| 호출 방법 | 메서드 호출 시 Stack Frame 생성 | JNI를 통해 OS 함수 호출 방식 사용 |