JVM(자바 가상 머신)은 자바 언어에서만 사용하는 것이 아니다. 코틀린, 스칼라 언어에서도 JVM 동작 방식을 그대로 따른다.
따라서 JVM을 정확히 이해하면 추후에 자바에서 파생된 모던 언어를 이해하는데 있어 수월해지며, 내부에서 정확히 어떻게 동작을 해서 코드가 실행이 되는지 개념을 알면 코드 최적화나 리팩토링을 하는데 매우 도움이 된다.
자바 코드를 컴파일하여 .class 바이트 코드로 만들면 이 코드가 JVM 에서 실행된다.
자바 가상 머신(JVM)의 동작 방식
1. 자바로 개발된 프로그램을 실행하면 JVM 은 OS 로부터 메모리를 할당합니다.
2. 자바 컴파일러(javac)가 자바 소스코드(.java)를 자바 바이트코드(.class)로 컴파일합니다.
3. Class Loader 를 통해 JVM Runtime Data Area 로 로딩합니다.
4. Runtime Data Area 에 로딩된 .class 들은 Execution Engine 을 통해 해석합니다.
5. 해석된 바이트 코드는 Runtime Data Area 의 각 영역에 배치되어 수행하며 이 과정에서 Execution Engine 에 의해
GC의 작동과 스레드 동기화가 이루어집니다.
자바 가상 머신(JVM)의 구조
ClassLoader System
JVM 내로 클래스 파일(*.class)을 동적으로 로드하고, 링크를 통해 배치하는 작업을 수행하는 모듈
즉, 로드된 바이트 코드(.class)들을 엮어서 JVM의 메모리 영역인 Runtime Data Area에 배치한다.
클래스를 메모리에 올리는 로딩 기능은 한번에 메모리에 올리지 않고, 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다.
Runtime Data Area
런타임 데이터 영역은 JVM 의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역.
OS가 관리하는 메인메모리인 RAM 의 일부 영역을 JVM 이 필요한 만큼 OS 로부터 할당받는다. OS 로부터 받은 메모리 공간을 Runtime Data Area 라고 부르며 5개의 영역으로 용도별로 나누어서 관리한다.
- 모든 스레드가 공유해서 사용
- 힙 영역 (Heap Area)
- JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역.
- new 연산자로 생성되는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳.
- 가비지 컬렉션에 대상이 되는 공간.
- 모든 Thread가 공유하기 때문에 동기화 문제가 발생할 수 있다.
- 메서드 영역(Method Area)
- Class Loader가 적재한 클래스(또는 인터페이스)에 대한 메타데이터 정보가 저장된다.
- 더 구제적으로는 Heap의 PermGen이라는 영역에 속한 영역인데, Java 8 이후로는 Metaspace라는 OS가 관리하는 영역으로 옮겨지게 된다.
- 힙 영역 (Heap Area)
- 스레드(Thread) 마다 하나씩 생성
- 스택 영역(Stack Area)
- 선입후출 구조
- int, long, boolean 등 기본 자료형을 생성할 때 저장하는 공간으로, 임시적으로 사용되는 변수나 정보들이 저장되는 영역
- 메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성되고 메서드 안에서 사용되는 값들을 저장하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다. 그리고 메서드 수행이 끝나면 프레임별로 삭제된다.
- PC 레지스터 (PC Register)
- 현재 수행중인 JVM 명령어 주소를 저장하는 공간.
- JVM 명령의 주소는 쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 가지고 있다.
- 연산 및 결과값을 메모리에 전달하기 전 CPU내 기억장치임.
- 스택 영역(Stack Area)
Execution Engine
실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.
실행 엔진은 이와 같은 바이트 코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경해준다.
이 수행 과정에서 실행 엔진은 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행한다.
인터프리터(Interpreter)
바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행한다.
JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다. 다만 같은 메소드 라도 여러번 호출이 된다면 매번 해석하고 수행해야 되서 전체적인 속도는 느리다.
JIT 컴파일러(Just-In-Time Compiler)
인터프리터는 바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행.
JIT Compiler Interpreter의 단점을 보완하기 위해 도입된 방식으로 반복되는 코드를 발견하여 바이트 코드 전체를 컴파일하여 Native Code로 변경하고 이후에는 해당 메서드를 더 이상 인터프리팅 하지 않고 캐싱해 두었다가 네이티브 코드로 직접 실행하는 방식.
하나씩 인터프리팅하여 실행하는 것이 아니라, 컴파일된 네이티브 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠르다.
하지만 바이트코드를 Native Code로 변환하는 데에도 비용이 소요되므로, JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고 인터프리터 방식을 사용하다 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어를 실행하는 식으로 진행한다.
Native Method Stack
네이티브 메서드 스택는 자바 코드가 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다.
또한 자바 이외의 언어(C, C++, 어셈블리 등)로 작성된 네이티브 코드를 실행하기 위한 공간이기도 하다.
사용되는 메모리 영역으로는 일반적인 C 스택을 사용한다.