- JVM은 Java가 가상 메모리를 사용하기 위한 VM이다.
- JVM 기반의 다른 프로그래밍 언어들도 존재한다 (Java를 대체하기 위해 나온 Kotlin이나 스칼라 등)
- program.java를 컴파일 하여 *.class 파일을 JVM에서 기계어로 바꾸어 실행시켜준다.
Virtual Memory와 Multiply mapped memory space
- JVM의 메모리 주소는 가상 메모리 주소를 사용한다. (물리적 메모리 주소가 아니다)
- 가상 메모리는 몇 가지 장점이 있다.
- 하나 이상의 가상 주소가 동일한 하나의 물리 주소를 가리킬 수 있다.
- 물리 메모리 공간보다 큰 가상 메모리 공간을 사용할 수 있다.
동작 방식
- JVM의 역할은 자바 앱을 클래스 로더를 통해 읽어 자바 API와 함께 실행하는 것이다.
- 자바 프로그램 실행 시 JVM은 OS로부터 메모리를 할당 받는다.
- 자바 컴파일러(javac)가 자바 소스코드(.java)를 자바 바이트 코드(.class)로 컴파일한다.
- Class Loader는 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크 하여 Runtime Data Area(실질적인 메모리를 할당 받아 관리하는 영역)에 올린다.
- Runtime Data Area에 로딩된 바이트 코드는 Execution Engine을 통해 해석된다.
- 이 과정에서 Execution Engine에 의해 Garbage Collector의 작동과 Thread 동기화가 이루어진다.
JVM의 구조
JVM 구성요소
- 클래스 로더(Class Loader)
- 실행 엔진(Execution Engine)
- 인터프리터(Interpreter)
- JIT 컴파일러(Just-in-Time)
- 가비지 컬렉터(Garbage Collector)
- 런타임 데이터 영역(Runtime Data Area)
- 메소드 영역
- 힙
- PC Register
- JVM 스택
- 네이티브 메소드 스택
- JNI (Java Native Interface)
- 네이티브 메소드 라이브러리
1. 클래스 로더 (Class Loader)
- 클래스 로더는 JVM 내로 클래스 파일(*.class)을 동적으로 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈이다.
- 즉, 로드된 바이트 코드(.class)들을 엮어서 JVM의 메모리 영역인 Runtime Data Areas에 배치한다.
- 클래스를 메모리에 올리는 로딩 기능은 한번에 메모리에 올리지 않고, 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다.
클래스 파일 로딩 순서
- Loading (로딩)
- 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
- Linking (링크)
- Verifying(검증) : 읽어들인 클래스가 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
- Preparing(준비) : 클래스가 필요로 하는 메모리를 할당한다.
- Resolving(분석) : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
- Initialization (초기화)
- 클래스 변수들을 적절한 값으로 초기화 한다. (static 필드들을 설정된 값으로 초기화 등)
2. 실행 엔진 (Execution Engine)
- 실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.
- 즉, 바이트 코드를 실제로 JVM내부에서 기계가 실행할 수 있는 형태로 변경해준다.
- 이 수행과정에서 실행 엔진은 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행한다.
인터프리터(Interpreter)
- 바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행한다.
- JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다.
- 다만, 같은 메소드라도 여러번 호출이 된다면 매번 해석하고 수행해야 되서 전체적인 속도는 느리다.
JIT 컴파일러(Just-In-Time Compiler)
- 인터프리터의 단점을 보완하기 위해 도입된 방식으로 반복되는 코드를 발견하여 바이트 코드 전체를 컴파일하여 Native Code로 변경하고, 이후에는 해당 메소드를 더 이상 인터프리팅 하지 않고 캐싱해 두었다가 네이티브 코드로 직접 실행하는 방식이다.
- Tip. 네이티브 코드는 JAVA에서 부모가 되는 C, C++, 어셈블리어로 구성된 코드를 의미한다.
가비지 컬렉터(Garbage Collector, GC)
- JVM은 가비지 컬렉터를 이용하여 Heap 메모리 영역에서 더는 참조되지 않는 객체의 메모리를 회수해 준다.
- Full GC(Major GC)가 발생하는 경우 GC를 제외한 모든 스레드가 중지되기 때문에 장애가 발생할 수 있다.
Tip. 수동으로 GC를 실행하기 위해 System.gc()로 gc실행 메소드를 호출할 수 있지만, 실제 실행은 보장되지 않는다.
3. 런타임 데이터 영역(Runtime Data Area)
런타임 데이터 영역은 JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.
1. Method Area(Static)
- 메소드 영역은 JVM이 시작될 때 생성되는 공간으로 바이트 코드(*.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.
- JVM이 동작하고 클래스가 로드될 때 적재되서 프로그램이 종료될 때까지 저장된다.
Tip. 메소드 영역(Method Area)는 Class Area나 Static Area로도 불린다.
- 모든 쓰레드가 공유하는 영역이라 다음과 같이 초기화 코드 정보들이 저장되게 된다.
- Field Info : 멤버변수의 이름, 데이터 타입, 접근 제어자의 정보
- Method Info : 메소드명, return타입, 매개변수, 접근 제어자의 정보
- Type Info : 클래스인지 인터페이스인지 여부 저장, Type의 속성, Super Class의 이름
- 메소드 영역에는 정적 필드와 클래스 구조만 갖고 있다고 할 수 있다.
📌 메소드 영역 / 런타임 상수 풀의 사용기간 및 스레드 공유 범위
- JVM 시작시 생성
- 프로그램 종료 시까지
- 명시적으로 null 선언 시
Runtime Constant Pool
- 메소드 영역에 존재하는 별도의 관리영역
- 각 클래스/인터페이스 마다 별도의 constant pool 테이블이 존재하는데, 클래스 생성할 때 참조해야할 정보들을 상수로 가지고 있는 영역이다.
- JVM은 이 Constant Pool을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조한다.
- 즉, 상수 자료형을 저장하고 참조하고 중복을 막는 역할을 한다.
- 요약
- 용도
- JVM에서 읽어들인 클래스와 인터페이스에 대한 런타임 상수 풀(Runtime Constant pool), 메소드와 필드, Static변수(클래스 변수), 메소드 바이트 코드 등을 보관.
- (클래스, 클래스 변수, 런타임 상수 풀, 메소드, 필드)
- JVM에서 읽어들인 클래스와 인터페이스에 대한 런타임 상수 풀(Runtime Constant pool), 메소드와 필드, Static변수(클래스 변수), 메소드 바이트 코드 등을 보관.
- 사용기간
- JVM 시작 시 생성
- 프로그램 종료 시까지 명시적으로 null 선언 시 GC 대상
- 구성방식이나 GC방법은 JVM벤더마다 약간 다를 순 있음.
- 쓰레드 공유
- 모든 쓰레드에서 공유
- Runtime Constant Pool
- Method Area 영역에 포함되지만 중요성을 띈다.
- 클래스 파일 constant_pool 테이블에 해당하는 영역.
- 클래스와 인터페이스 상수, 메소드와 필드에 대한 모든 레퍼런스 저장.
- JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조.
- 용도
2. Heap Area
- 힙 영역은 메소드 영역과 함께 모든 쓰레드가 공유하며, JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이다.
- new 연산자로 생성되는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳이다.
<aside> 📌 힙 영역의 사용기간 및 쓰레드 공유 범위
- 객체가 더 이상 사용되지 않거나 명시적으로 null 선언 시
- GC(Garbatge Collection) 대상
</aside>
- 유의할 점은 힙 영역에 생성된 객체와 배열을 Reference Type으로써, JVM 스택 영역의 변수나 다른 객체의 필드에서 참조된다는 점이다.
- 즉, 힙의 참조 주소는 스택이 갖고 있고 해당 객체를 통해서만 힙 영역에 있는 인스턴스를 핸들링 할 수 있는 것이다.
- 만일 참조하는 변수나 필드가 없다면 의미 없는 객체가 되기 때문에 이것을 쓰레기로 취급하고 JVM은 쓰레기 수집기인 Garbage Collector를 실행시켜 쓰레기 객체를 힙 영역에서 자동으로 제거된다.
- 이처럼 힙 영역은 가비지 컬렉션에 대상이 되는 공간이다.
- 효율적인 가비지 컬렉션을 수행하기 위해 세부적으로 다음과 같이 5가지 영역으로 나뉘게 된다.
- 이렇게 다섯가지 영역(Eden, survivor 0, survivor 1, Old, Permanent)으로 나뉜 힙 영역은 다시 물리적으로 Young Generation 과 Old Generation 영역으로 구분되게 되는데 다음과 같다.
- Young Generation : 생명 주기가 짧은 객체를 GC 대상으로 하는 영역.
- Eden : new를 통해 새로 생성된 객체가 위치. 정기적인 쓰레기 수집 후 살아남은 객체들은 Survivor로 이동
- Survivor 0 / Survivor 1 : 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 순차적으로 이동
- Old Generation : 생명 주기가 긴 객체를 GC 대상으로 하는 영역. Youn Generation에서 마지막까지 살아남은 객체가 이동
- 요약
- 용도
- 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 메모리 영역.
- new연산자를 통해 생성한 객체, 또는 인스턴스와 배열을 저장.
- JVM이 관리
- 사용기간
- 객체가 더 이상 쓰지 않거나, 명시적으로 null 선언시 GC 대상
- 구성방식이나 GC방법은 JVM 벤더마다 다를 수 있음.
- 쓰레드 공유
- 모든 쓰레드에서 공유
- 용도
3. Stack Area
- 스택 영역은 int, long, boolean 등 기본 자료형을 생성할 때 저장하는 공간으로, 임시적으로 사용되는 변수나 정보들이 저장되는 영역이다.
- 메서드 호출 시마다 각각의 **스택 프레임(그 메서드만을 위한 공간)**이 생성되고 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다.
- 그리고 메서드 수행이 끝나면 프레임별로 삭제된다.
<aside> 📌 [ 스택 프레임(stack frame) ]
메소드가 호출될 때마다 프레임이 만들어지며, 현재 실행중인 메소드 상태 정보를 저장하는 곳이다
메서드 호출 범위가 종료되면 스택에서 제거된다.
스택 프레임에 쌓이는 데이터는 메서드의 매개변수, 지역변수, 리턴값, 연산시 결과값 등이 있다
</aside>
- 각 쓰레드마다 하나씩 존재하며 스택 프레임의 개념도 결국 스레드에 스택이 할당 되는 걸 말한다.
- 쓰레드를 추가 생성하지 않고 그냥 메소드를 돌리면 main쓰레드에서 돈다. 시스템 쓰레드 같은건 GC나 JVM이 작동되기 위해 필요한 시스템 메소드들(네이티브 메소드 같은거)가 실행 됨.
- 요약
- 용도
- 선입후출 구조(FILO). 메소드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame 저장.
- 메소드 정보, 지역변수, 매개변수, 연산 중 발생하는 임시 데이터 저장.
- 사용기간
- 메소드 블럭 {} 이나 메소드가 끝날 때.
- 쓰레드 공유
- 각 쓰레드 별로 생성
- 용도
4. PC 레지스터
- PC레지스터는 쓰레드가 시작될 때 생성되며, 현재 수행중인 JVM 명령어 주소를 저장하는 공간이다.
- JVM 명령의 주소는 쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 가지고 있다.
- 일반적으로 프로그램의 실행은 CPU에서 명령어(Instruction)을 수행하는 과정으로 이루어진다.
- 예를들어, A와 B라는 데이터와 피연산 값인 Operand가 있고 이를 더하라는 연산 Instruction이 있다고 하자.
- A와 B, 그리고 더하라는 연산이 순차적으로 진행이 되게 되는데, 이때 A를 받고 B를 받는 동안 이 값을 CPU가 어딘가에 기억해 두어야 할 필요가 생긴다.
- 이 공간이 바로 CPU 내의 기억장치 Register이다.
- 하지만 자바의 PC Register는 위의 cpu Register와 다르다.
- 자바는 OS나 CPU의 입장에서는 하나의 프로세스이기 때문에 가상 머신(JVM)의 리소스를 이용해야 한다.
- 그래서 자바는 CPU에 직접 연산을 수행하도록 하는 것이 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며, 이를 위한 버퍼 공간으로 PC Register라는 메모리 영역을 만들게 된 것이다
- 따라서 JVM은 스택에서 비연산값 Operand를 뽑아 별도의 메모리 공간인 PC Register에 저장하는 방식을 취한다.
- 만약에 스레드가 자바 메소드를 수행하고 있으면 JVM 명령(Instruction)의 주소를 PC Register에 저장한다.
- 그러다 만약 자바가 아닌 다른 언어(C언어, 어셈블리)의 메소드를 수행하고 있다면, undefined 상태가 된다.
- 왜냐하면 자바에서는 이 두 경우를 따로 처리하기 때문이다.
- 이 부분이 바로 뒤에 언급하게 될 Native Method Stack 공간이다.
- 요악
- 용도
- 현재 실행 중인 JVM 주소를 가지고 있다.
- CPU 명령어. 즉, Instruction을 수행한다.
- CPU instruction 수행하는 동안 필요한 정보를 CPU 내 기억장치인 레지스터에 저장.
- 연산 및 결과값을 메모리에 전달하기 전 CPU내 기억장치임.
- 쓰레드 공유
- 각 쓰레드별로 생성
5. Native Method Stack Area
- 네이티브 메서드 스택는 자바 코드가 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다.
- 또한 자바 이외의 언어(C, C++, 어셈블리 등)로 작성된 네이티브 코드를 실행하기 위한 공간이기도 하다.
- 사용되는 메모리 영역으로는 일반적인 C 스택을 사용한다.
- 위에서 배운 JIT 컴파일러에 의해 변환된 Native Code 역시 여기에서 실행이 된다고 보면 된다
- 일반적으로 메소드를 실행하는 경우 JVM 스택에 쌓이다가 해당 메소드 내부에 네이티브 방식을 사용하는 메소드가 있다면 해당 메소드는 네이티브 스택에 쌓인다.
- 그리고 네이티브 메소드가 수행이 끝나면 다시 자바 스택으로 돌아와 다시 작업을 수행한다.
- 그래서 네이티브 코드로 되어 있는 함수의 호출을 자바 프로그램 내에서도 직접 수행할 수 있고 그 결과를 받아올 수도 있는 것이다.
- 네이티브 메소드 스택은 바로 다음에 배울 **네이티브 메소드 인터페이스(JNI)**와 연결되어 있는데, JNI가 사용되면 네이티브 메서드 스택에 바이트 코드로 전환되어 저장되게 된다.
출처
https://inpa.tistory.com
Java 공식문서
'프로그래밍 언어 > Java' 카테고리의 다른 글
GC (Garbage Collection) (0) | 2024.02.25 |
---|---|
JVM 메모리 구조 (0) | 2024.02.25 |