- 역할 : 자바 애플리케이션을 클래스 로더를 통해 읽어, 자바 API와 함께 실행하는 것
- 자바 소스 파일을 어떤 동작으로 코드를 읽는 지, 간단한 요약 도식
자바 프로그램 실행 시, JVM은 OS로부터 메모리 할당 받음
자바 컴파일러(javac)가 자바 소스코드(.java) → 자바 바이트코드(.class)로 컴파일
Class Loader는 동적 로딩을 통해, 필요한 클래스들을 로딩 및 링크 하여, Runtime Data Area(실질적인 메모리를 할당받아 관리하는 영역)에 올림.
Runtime Data Area에 로딩 된 바이트 코드는 Execution Engine을 통해 해석됨
4번 과정에서 Execution Engine에 의해 Garbage Collector 작동 및 Thread 동기화 이루어짐.
JVM의 구성 요소
- 요약 도식의 Class Loader ↔ Execution Engine ↔ Runtime Data Area 부분의 상세화한 도식
클래스 로더(Class Loader)
실행 엔진(Execution Engine)
인터프리터(Interpreter)
JIT 컴파일러(Just In Time)
가비지 콜렉터(Garbage Collector)
런타임 데이터 영역(Runtime Data Area)
Method 영역
Heap 영역
스택 영역
PC Register
Native Method Stack
JNI(Native Method Interface) - 네이티브 메서드 인터페이스
네이티브 메서드 라이브러리(Native Method Library)
클래스 로더(Class Loader)
클래스 로더 : JVM 내로 클래스파일(.class)을 동적으로 로드 및 링크를 통해 배치하는 작업 수행 모듈 💡 즉, 로드된 바이트코드들을 엮어, JVM 메모리 영역인 Runtime Data Area에 배치!!
로딩 기능은 한번에 메모리에 올리지 않음 ⇒ 애플리케이션에서 필요한 경우 동적으로 메모리에 적재
클래스 파일의 로딩 순서 : Loading → Linking → Initialization
Loading(로드) : 클래스 파일을 가져와 JVM 메모리에 로드
Linking(링크) : 클래스 파일을 사용하기 위한 검증 과정
Verifying(검증) : 읽어들인 클래스가 JVM 명세에 명시된 대로 구성되어 있는지 검사
Preparing(준비) : 클래스가 필요로 하는 메모리 할당
Resolving(분석) : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스 → 다이렉트 레퍼런스로 변경
Initialization(초기화) : 클래스 변수들을 적절한 값으로 초기화 (static 필드들을 설정된 값으로 초기화 등)
실행 엔진(Execution Engine)
: Class Loader를 통해 Runtime Data Area에 배치된 바이트 코드를 명령어 단위로 읽어 실행
1. 바이트코드(.class)는 기계가 바로 수행 가능한 언어 X ⇒ JVM이 이해 가능한 중간 레벨로 컴파일된 코드 ⇒ 실행 엔진이 이를 로우 레벨 언어로 변경해줌!!
2. 실행 엔진 수행 과정
Interpreter : 바이트 코드 명령어를 하나씩 읽어 해석 후 바로 실행
JVM 안에서 바이트 코드는 기본적으로 Interpreter 방식으로 동작 ⇒ 단점 : 같은 메서드 여러 번 호출 → 매번 해석 및 수행 → 전체적인 속도 ↓
JIT Compiler(Just-In-Time)
Interpreger의 단점 보완을 위한 방식
반복되는 코드를 발견하여, 바이트 코드 전체를 컴파일하여, Native Code로 변경 후, 이후 해당 메서드를 캐싱해두고, Native Code로 직접 실행하는 방식
⇒ 장점 : faster than Interpreter 단점 : 바이트 코드 → Native Code 변환에 비용 소모 됨 ⇒ 모든 코드를 JIT 방식을 사용하지는 X ⇒ 인터프리터 방식 사용 중 일정 기준 넘어갈 시 JIT 사용
Garbage Collector(GC)
JVM은 GC를 통해, Heap 메모리 영역에서 더 이상 사용되지 않는 메모리 자동 회수
일반적으로 자동으로 실행됨, But GC가 실행되는 시간은 정해져 있지 않음! 특히, Full GC 발생 시, GC 제외 모든 Thread 중지! ⇒ 장애 발생 가능
수동으로 System.gc() 메서드 사용 가능, But 실제 실행 보장 X
Runtime Data Area(런타임 데이터 영역)
Java Application 실행 시 사용되는 Data 적재 영역 => 쉽게 설명하면, JVM의 메모리 영역!
Method Area, Heap Area → 모든 Thread가 공유하는 영역
Stack Area, PC Register, Native Method Stack → 각 쓰레드마다 생성되는 개별 영역
위의 설명 도식화
1. Method Area(메서드 영역) : JVM 시작될 때 생성되는 공간
바이트 코드(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 공간
JVM이 동작하고 클래스가 로드될 때 적재되어, 프로그램 종료 시까지 저장됨.
Class Area or Static Area라고도 불리움.
모든 스레드가 공유하는 영역 → 다음과 같은 초기화 코드 정보들이 저장되게 함
Field Info : 멤버 변수명, Data Type, 접근 제어자 정보
Method Info : 메소드명, Return Type, 함수 인자, 접근 제어자 정보
Type Info : Class / Interface 여부 저장, Type의 속성, 이름, Super Class의 이름
⇒ 간단히 말해, 메서드 영역에는 정적 필드, 클래스 구조만을 가짐
Runtime Constant Pool
메서드 영역에 존재하는 별도 관리 영역
각 Class/Interface 마다 별도의 Constant Pool 테이블이 존재 == 클래스 생성 시 참조해야 할 정보들을 상수로 가지고 있는 영역
JVM은 이 Constant Pool을 통해, 해당 메서드 or 필드의 실제 메모리 상 주소를 찾아 참조
즉, 상수 자료형을 저장하여 참조하고, 중복을 막는 역할 수행!
2. Heap Area(힙 영역)
: 모든 Thread가 공유하며, JVM이 관리하는, 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역
즉, new 연산자로 생성되는 클래스 및 인스턴스 변수, 배열 타입 등의 Reference Type이 저장되는 공간
당연히 Method Area 영역에 저장된 클래스만이 생성되어 적재 됨! * 힙 영역의 사용 기간 및 Thread 공유 범위 : 객체가 더 이상 사용되지 않거나 or Null로 명시적 선언 시 GC 대상
Heap 영역에 생성된 객체 및 배열 is Reference Type ⇒ JVM Stack Area의 변수 or 다른 객체의 필드에서 참조 됨 ⇒ 힙의 참조 주소 → 스택이 가짐 ⇒ 해당 객체를 통해서만 힙 영역의 인스턴스 핸들링 가능
참조하는 변수 or 필드 X ⇒ 의미없는 객체로 판별 ⇒ GC 대상이 되어 Heap에서 자동 삭제
Heap의 5가지 영역 - 효율적인 GC를 위해!!
5가지 영역은 다시 Young Generation 과 Old Generation 으로 나뉨
Young Generation : 생명 주기가 짧은 객체를 GC 대상으로 하는 영역
Eden : new 를 통해 새로 생성된 객체가 위치함.
정기적인 쓰레기 수집 후, 살아남은 객체들은 Survivor로 이동
Survivor 0, Survivor 1 : 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 순차적으로 이동
Old Generation : 생명 주기가 긴 객체를 GC 대상으로 하는 영역
Young Generation에서 마지막까지 살아남은 객체가 이동
3. Stack Area(스택 영역) : int, long 등의 기본 자료형 생성 시 저장하는 공간
메서드 호출 시마다 각각의 Stack Frame(해당 메서드를 위한 공간)이 생성되고, 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개변수, 지역변수, return값 및 연산 시 발생하는 값들을 임시 저장 ⇒ 수행 종료 시 프레임 별로 삭제
💡 데이터에 따른 Stack, Heap에 저장되는 방식의 차이 1. 기본 자료형 → Stack 영역에 직접 값 가짐 2. 참조 타입형 → Heap or Method 영역의 객체 주소를 가짐
예를들어 Person p = new Person(); 와 같이 클래스를 생성할 경우, new 에 의해 생성된클래스는 Heap Area 에 저장되고, Stack Area 에는 생성된 클래스의 참조인 p 만 저장된다.
⇒ new의 남용을 피할 것!
각 Thread 마다 개별적으로 가짐, Thread 시작될 때 할당됨.
프로세스가 메모리에 Load 될 때, Stack 사이즈 고정됨 → 런타임 시 변경 불가! ⇒ 프로그램 실행 중, 크기 부족 → StackOverFlowError 발생
4.Program Counter Register(PC 레지스터) : 현재 수행중인 JVM 명령어 주소 저장 공간
Thread 시작 시 생성됨.
사전 지식 - CPU Register
일반적인 프로그램의 실행은 CPU에서 명령어(Instruction)을 수행하는 과정으로 이루어짐
CPU는 연산 수행에 필요한 정보를 Register라는 CPU 내 기억장치 사용 ⇒ JVM의 PC Register는 CPU Register와 다름!!!
JVM == 하나의 프로세스(by OS/CPU) ⇒ CPU에 직접 연산을 수행하도록 하는 것이 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공해야 함 ⇒ 이를 위한 버퍼 공간 == PC Register
⇒ JVM은 Stack에서 비연산값 Operand를 뽑아 별도의 메모리 공간인 PC Register에 저장하는 방식 사용
만약 Thread가 Java Method 수행 중일 때, ⇒ JVM 명령(Instruction)의 주소 → PC Register에 저장
그러다가 만약 Java가 아닌 다른 언어(C, 어셈블리)의 Method 수행 중일 시, ⇒ undefined 상태 됨.
why? Java에서는 두 경우를 따로 처리!! ⇒ Native Method Stack 공간!!!!
5.Native Method Stack(네이티브 메서드 스택)
: 바이트 코드가 아닌 실제 실행 가능한 기계어로 작성된 프로그램을 실행시키는 영역 + 자바 이외의 언어로 작성된 코드 실행을 위한 공간
사용되는 메모리 영역으로는 일반적인 C 스택 사용
JIT 컴파일러에 의해 변환된 Native Code 또한 여기서 실행됨
JVM 스택에 쌓이다가 해당 메서드 내부에 Native 방식의 메서드 존재 시 ⇒ 해당 메서드를 Native Method Stack에 쌓음 ⇒ Native Code 함수를 Java 프로그램 내에서도 직접 수행 가능하며, 결과도 받을 수 있음