✔️ JVM의 구조
✔️ JAVA 가상머신의 동작과정
JVM은 자바.class파일을 클래스 로더를 통해 읽어와서 자바 API와 함께 실행한다.
1. 자바 프로그램을 실행하면 JVM은 OS로부터 메모리를 할당받는다
2. 자바 컴파일러가(javac)가 자바 소스코드(.java)를 자바 바이트 코드(.class)로 컴파일한다.
3. class Loader는 동적로딩을 통해 필요한 클래스를 로딩 및 링크하여 Runtime Data Area에 올린다.
4. Runtime Data Area에 올라간 바이트코드는 Excution Engine을 통해서 해석된다.
5. 이과정에서 Excution Engine에 의한 GC의 작동과 Thread 동기화가 이루어진다.
✔️ JVM의 상세 구조
Class Loader
JVM으로 바이트코드(.class)를 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈
로드된 바이트 코드들을 엮어서 JVM의 메모리 영역인 Runtime Data Areas에 배치함
로딩기능은 한번에 메모리에 올리는 것이 아니라, 어플리케이션에서 필요할때마다 동적으로 메모리를 적재한다.
로딩은 3단계로 구성됨
Loading -> Linking -> Initializaion
- Loading(로드): 클래스 파일을 가져와서 JVM의 메모리에 로드
- Linking(링크): 클래스 파일을 사용하기 위해 검증하는 과정이다
- Verifying(검증) : 읽어들인 클래스가 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
- preparing(준비) : 클래스가 필요로 하는 메모리를 할당한다.
- Resolving(분석) : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
- Initialization(초기화) : 클래스 변수들을 적절한 값으로 초기화한다. ( static 필드들을 설정된 값으로 초기화 등 )
Excution Engine
실행 엔진은 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행
인터프리터(Interpreter)
바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행한다.
JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다.
다만 같은 메소드 라도 여러번 호출이 된다면 매번 해석하고 수행해야 되서 전체적인 속도는 느리다.
JIT 컴파일러(Just-In-Time Compiler)
위의 Interpreter의 단점을 보완하기 위해 도입된 방식으로 반복되는 코드를 발견하여 바이트 코드 전체를 컴파일하여 Native Code로 변경하고 이후에는 해당 메서드를 더 이상 인터프리팅 하지 않고 캐싱해 두었다가 네이티브 코드로 직접 실행하는 방식이다.
하나씩 인터프리팅하여 실행하는것이 아니라, 컴파일된 네이티브 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠르다.
하지만 바이트코드를 Native Code로 변환하는 데에도 비용이 소요되므로, JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고 인터프리터 방식을 사용하다 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어를 실행하는 식으로 진행한다.
런타임 데이터 영역(Runtime Data Area)
Java에서 Thread가 공유하는 영역과 공유하지 않는 영역은 다음과 같습니다.
- Thread가 공유하는 영역 (Java)
- 힙 영역
- 메서드 영역
- Thread가 공유하지 않는 영역 (Java)
- Stack 영역
- PC 레지스터 영역
- 네이티브 메서드 스택
스택 영역(Stack)
스택 영역에는 메서드 호출 시 지역 변수, 매개변수, 함수 호출내역 등이 저장되는 영역입니다. 각 스레드마다 개별적으로 생성되며, 메서드 호출 시 생성되었다가 메서드가 종료되면 사라집니다.
스택에 저장되는 데이터들은 Frame 이라는 자료구조로 저장이 됩니다. 그리고 Frame 에는 아래와 같은 데이터들이 저장됩니다.
- Local Variables
- Operand stack
- Frame data
Local Variables
Local Variables 에는 이름에서 알 수 있듯이, 함수에서 쓰이는 매개변수 혹은 지역변수들이 저장됩니다. 함수 내에 몇 개의 지역변수가 있을지에 대해서는 컴파일 시점에 정해지고, 실행 시점에 메모리가 할당됩니다.
Operand Stack
Operand Stack은 피연산자들을 stack 자료구조로 저장해두는 것을 의미합니다. 예시로, 곱하기 연산이 로직에 있다면 Operand Stack에서 두 개의 피연산자를 꺼내어 계산하고 다시 Operand stack에 결과를 저장합니다.
메서드 영역 (Method Area)
메서드 영역은 JVM이 시작될 때 생성되는 공간으로 바이트 코드(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.
JVM이 동작하고 클래스가 로드될 때 적재되서 프로그램이 종료될 때까지 저장 된다.
PC 레지스터(Program Counter Register)
PC 레지스터 영역 은 각 스레드마다 현재 수행 중인 JVM 명령의 주소가 저장되는 영역입니다. Thread 는 각자의 메소드를 실행하게 됩니다. 이때, Thread 별로 동시에 실행하는 환경이 보장되어야 하므로 최근에 실행 중인 JVM 에서는 명령어 주소값을 저장할 공간이 필요합니다.
이 부분을 PC 레지스터 영역이 관리하여 추적이 가능하게 만들어줍니다. PC 레지스터 영역이 있음로써, Thread 들은 각각 자신만의 PC 레지스터들을 가지고 동작할 수 있습니다.
만약 실행했던 메소드가 네이티브하다면, 해당 명령어의 위치를 알 수 없기 때문에 PC 레지스터에 undefined 값을 기록하게 됩니다. 실행했던 메소드가 네이티브하지 않다면, PC 레지스터는 JVM 에서 사용된 명령의 주소 값을 저장하게 됩니다.
네이티브 메서드 스택(Native Method Stack)
위의 PC 레지스터 영역에서 네이티브 메서드들은 PC 레지스터에 저장이 되지 않는다고 했습니다. 즉, 네이티브 메서드들을 위한 영역이 별도로 필요하다는 것을 알 수 있습니다. 바로 이 네이티브 메서드 스택(Native Method Stack) 영역이 네이티브 메서드들을 실행하기 위한 스택 영역입니다.
네이티브 메서드 스택(Native Method Stack) 영역 또한 스택 영역과 마찬가지로 각 Thread 마다 개별적으로 생성되며, 자바 외부의 네이티브 코드를 호출할 때마다 생성되었다가 호출이 완료되면 사라집니다.
힙 영역(Heap)
힙 영역(Heap)은 객체와 배열이 할당되는 영역입니다. 자바에서는 new 키워드로 객체와 배열을 생성하며, 생성된 객체와 배열의 크기에 따라 크기가 동적으로 변하는 특징을 가지고, 모든 Thread에서 공유되는 특징을 가집니다.
그리고 힙 영역은 JVM이 실행 중인 시스템 메모리 중 가장 큰 부분을 차지하며, 가비지 컬렉션(Garbage Collection)이 동작하는 영역입니다. GC가 어떻게 동작하는지에 대해선 따로 포스팅할 예정이고, 이 글에선 힙 영역이 어떻게 세분화되어 있는지 살펴보겠습니다.
힙 영역 - java 7 이전 vs java 8 이후
Java 7 이전 버전의 JVM은 위의 그림처럼 PermGen 이 Heap 영역에 포함되었습니다. 하지만 Java8부터 JVM의 메모리 영역 중 Permanent Generation 메모리 영역이 사라지고 Metaspace 영역이 생겼습니다. 또한 Metaspace 영역은 Heap 영역에 포함되지 않으며, Native Memory에 속하게 되었습니다.
이미지 및 출처)
'🍎 Backend > JAVA' 카테고리의 다른 글
[JAVA] Garbage Collector, GC (0) | 2025.01.30 |
---|---|
[JAVA] String, StringBuffer, 예외(Exception) (0) | 2025.01.09 |
[JAVA] 제네릭, 지네릭스(Generics) (0) | 2025.01.09 |
[JAVA] 어노테이션(Annotation) (0) | 2025.01.09 |
[JAVA] 람다(Lambda)와 스트림(Stream) (0) | 2025.01.09 |