Study
JVM의 구조와 동작 원리
kanado
2025. 1. 23. 17:50
자바 개발자라면 꼭 알아야 할 JVM(Java Virtual Machine)의 구조와 동작 원리에 대해 정리해보았다. 자바 프로그램이 어떻게 실행되는지, JVM 내부에서 어떤 일이 일어나는지를 이해하면, 더 최적화된 코드 작성과 문제 해결에 큰 도움이 될 것이라고 생각한다.
1. JVM (Java Virtual Machine)
- 3가지 핵심 구성 (Class Loader, Runtime Data Area, Execution Engine)
- Class Loader: java 컴파일을 통해 동적 로딩(Dynamic loading)으로 프로그램 실행 중에 필요한 시점에 해당 클래스의 바이트 코드만 실시간으로 Runtime Data Area에 적재한다.
동적 로딩 (Dynamic Loading): 프로그램 실행 시 필요한 클래스나 리소스를 미리 로드하지 않고, 필요할 때 실시간으로 로드하여 메모리에 적재하는 방식. 이는 정적 로딩(Static Loading)과 대비되는 개념으로, 정적 로딩은 프로그램 실행 전에 모든 필요한 리소스와 클래스를 메모리에 미리 로드하는 것을 말한다.
- Runtime Data Area에 적재하는데 3가지 단계( 동적 로딩의 동작 과정 )
- 로딩: 클래스 파일.class을 찾아서 읽고, 해당 클래스의 바이트코드를 메모리의 (Runtime Data Area) Method Area에 적재.
- 링크: 로딩된 .class 파일은 JVM에서 실행할 수 있도록 검증(안전한지), 준비(변수와 기본값을 위한 메모리), 해석(실제 메모리 주소로 변환) 과정을 거쳐 실행될 준비 완료.
- 초기화: 클래스 및 정적 코드 블록(바로 실행해도 문제없는)을 실행합니다.
2. Runtime Data Area: JVM이 프로그램을 실행하기 위해 사용하는 메모리 영역으로 해당 영역은 프로그램 실행 중 생성되는 다양한 데이터를 저장하고 관리하는데 사용된다.
- 5가지 구성 요소
- PC Register: 각 스레드가 어떤 부분(코드)을 어떤 명령(CPU)으로 실행해야 할지 관리하고 기록(현재 실행 중인 명령의 주소를 저장)
- JVM Stack (JVM 스택 영역): JVM 스택은 스레드마다 고유하게 존재하는 영역입니다. 각종 메서드 실행 상태와 지역 변수를 저장.
- Native Method Area: 네이티브 코드(C, C++ 등 비-Java 코드) 실행 시 사용.
- Heap (힙 영역): 힙은 JVM 메모리가 관리하는 부분 중 가장 큰 부분으로 모든 스레드가 공유하며 자바 애플리케이션이 생성한 모든 객체와 배열이 저장됩니다. (힙 영역에서 사용되지 않는 객체는 GC(가비지 컬렉터)에 의해 제거되어 메모리 공간을 확보)
- Heap의 String 영역 (String Pool): java에서는 문자열을 효율적으로 관리하기 위해 Heap 내에 String Pool이라는 특별한 공간을 제공한다. String Pool은 중복된 문자열 객체를 제거하고 메모리 사용을 최적화하기 위해 사용된다. String Pool에 저장된 문자열은 불변이다. 즉, 한 번 생성되면 변경할 수 없다. 하지만 new String()을 사용하면 문자열이 String Pool에 저장되지 않고 Heap의 일반 영역에 새로운 객체로 생성되면서 중복성을 확인하지 않아 메모리 낭비 하게된다.
- Method Area (메소드 영역): 프로그램 실행에 필요한 클래스, 인터페이스 와 메타데이터(클래스 구조, 메서드, 정적(static)변수, 상수 등)를 바이트 코드로 저장. 또한 클래스는 JVM이 실행 중일 때 최초로 참조될 때 한 번만 로드되며, 이후에는 Method Area에 저장된 내용을 사용.Runtime Data Area: JVM이 프로그램을 실행하기 위해 사용하는 메모리 영역으로 해당 영역은 프로그램 실행 중 생성되는 다양한 데이터를 저장하고 관리하는데 사용된다.
Method Area |
클래스 구조 및 메타데이터 저장 | 클래스 메타데이터, 메서드 메타데이터, 상수, 정적(static)변수 |
모든 스레드가 공유 | JVM이 종료될 때까지 유지 |
Heap | 객체와 배열 저장 | 객체, 배열, 객체 내부의 멤버 변수 | 모든 스레드가 공유 | Garbage Collector가 관리 |
Stack | 메서드 호출과 지역 변수 관리 | 지역 변수, 메서드 호출 정보, 참조 변수 | 스레드별로 독립 | 메서드 종료 시 자동 해제 |
클래스 메타데이터: 클래스 이름, 해당 클래스의 부모 클래스 이름, 구현한 인터페이스 목록,클래스가 abstract인지, final인지 등의 접근 제한자 정보
3. Execution Engine: 메모리에 로드된 바이트코드를 실제 기계어로 OS에 맞게 변환하고 Runtime Data Area에서 필요한 데이터를 가져와 실행한다.
- 3가지 구성 요소
- 인터프리터(Interpreter): 인터프리터는 바이트코드를 한 줄씩 읽어서 기계어로 변환하고 실행한다. but 같은 코드를 반복해서 실행할 때마다 매번 다시 변환해야 하므로 실행 속도가 느린 성능 문제 존재했으나, JIT 컴파일러의 도입으로 많은 최적화가 이루어졌다.
- Just-In-Time (JIT) 컴파일러: 실행 중인 애플리케이션의 반복적으로 실행되는 부분을 감지하여, 그 부분만 기계어로 미리 컴파일한다. 이렇게 미리 컴파일된 코드는 고속으로 실행할 수 있으며, 프로그램의 전체 실행 속도를 향상.
- 가비지 컬렉터(Garbage Collector): 힙 메모리 영역에서 더 이상 참조되지 않는 객체들을 자동으로 검출하고 제거한다. 이 과정은 메모리를 효율적으로 관리하며, 메모리 누수와 같은 문제를 방지한다.
메모리 누수(Memory Leak): 프로그램이 동작하면서 더 이상 필요하지 않은 메모리(사용하지 않는 객체)가 해제되지 않고 계속 메모리 공간을 차지하는 상태
2. JVM에서 실행되는 큰그림 순서대로 정리
- 소스 코드 작성 (example.java)
- 컴파일 (javac) → 바이트코드 생성 (example.class)
- JVM 실행 시작
- Class Loader
- 클래스 로드 (Method Area에 메타데이터 저장)
- Runtime Data Area
- Method Area: 클래스 정보, 정적 변수
- Heap: 객체, 배열 저장
- Stack: 지역 변수, 메서드 호출 정보
- Execution Engine
- 바이트코드 해석 및 실행 (Interpreter & JIT Compiler)
- 필요 시 Garbage Collector가 메모리 관리
3. Execution Engine과 Runtime Data Area가 동시에 작동하는 방식
- 메서드 호출
- JVM이 특정 메서드를 호출하면, Execution Engine은 해당 메서드의 바이트코드를 Method Area에서 가져온다.
- 메서드 호출에 필요한 매개변수는 Stack에 저장된다.
- 객체 생성
- 객체를 생성하면 Execution Engine은 Heap 영역에 객체를 생성한다.
- 객체 참조는 Stack에 저장된다.
- 변수 연산
- 메서드가 실행되면서, 지역 변수는 Stack에 저장된다.
- 필요한 데이터는 Heap이나 Method Area에서 읽어오고, 연산 결과를 다시 저장한다.
- Garbage Collection
- Execution Engine은 Heap 영역의 객체 중 참조되지 않는 객체를 Garbage Collector에 의해 제거하도록 관리한다.