🧩 BE
home

JVM 내부 구조

Date
2024/03/20
Category
Programming Language
Tag
Java
Detail

JVM의 동작 방식

자바 가상 머신(Java Virtual Machine)인 JVM의 역할은 위의 컴파일된 .class 파일을 클래스 로더를 통해 읽어 자바 API와 함께 실행하는 것이다.
다음은 자바 소스 파일을 어떤 동작으로 코드를 읽는지에 대한 간단한 요약 도식이다.
1.
자바 프로그램을 실행하면 JVM은 OS로부터 메모리를 할당받는다.
2.
자바 컴파일러(javac)가 자바 소스코드(.java)를 자바 바이트 코드(.class)로 컴파일한다.
3.
Class Loader는 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크하여 Runtime Data Area(실질적인 메모리를 할당받아 관리하는 영역)에 올린다.
4.
Runtime Data Area에 로딩된 바이트 코드는 Execution Engine을 통해 해석된다.
5.
이 과정에서 Execution Engine에 의해 Garbage Collector의 작동Thread 동기화가 이루어진다.

JVM Runtime Data Area

JVM 명세(docs.oracle.com)를 따르기만 한다면 누구나 JVM을 개발하여 제공할 수 있다. 대표적으로 오라클의 핫스팟 JVM, Amazon Corretto JVM 등 다양한 JVM이 존재한다. 이번엔 JVM 명세를 바탕으로 위에서 다뤄본 JVM의 런타임 데이터 영역에 대해서 알아보자.
자바 가상 머신은 프로그램 실행 중 다양한 런타임 데이터 영역을 사용한다. 런타임 데이터 영역은 모든 스레드들이 공유하는 영역스레드 별 할당되는 개별 영역으로 구분된다.
JVM을 시작하면 Heap 영역Method 영역이 생성되며 해당 영역들은 모든 스레드들이 공유한다. 각 스레드가 시작될 때마다 스레드별로 PC Register, Stack, Native Method Stack이 생성되며 스레드가 종료될 때 사라진다. 마지막으로 모든 스레드들이 실행되고 종료되면 JVM이 종료되면서 Heap 영역과 Method 영역도 사라진다.

메서드 영역 (Method Area)

메서드 영역은 클래스별 정보, 정적 필드를 저장하는 공간이다(때문에 Class Area나 Static Area로도 불린다). JVM이 시작할 때 생성되는 공간으로, 바이트 코드(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다.
모든 스레드가 공유하는 영역이라 다음과 같이 코드 정보들이 저장된다.
Field Info : 멤버 변수의 이름, 데이터 타입, 접근 제어자의 정보
Method Info : 메소드 이름, return 타입, 함수 매개변수, 접근 제어자의 정보
Type Info : Class인지 Interface인지 여부, Type의 속성, 이름, Super Class의 이름

Runtime Constant Pool

메서드 영역에 존재하는 별도의 관리 영역.
각 클래스/인터페이스 마다 별도의 constant pool 테이블이 존재하는데, 클래스 생성할 때 참조해야할 정보들을 상수로 가지고 있는 영역이다. JVM은 이 Constant Pool을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조한다.
상수 자료형을 저장하여 참조하고 중복을 막는 역할을 수행

힙 영역 (Heap Area)

힙 영역은 메서드 영역과 함께 모든 스레드가 공유하며, JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이다.
new 연산자로 생성되는 클래스와 인스턴스 변수, 배열 타입 등 참조 자료형(Reference Type)이 저장되는 곳이다. 당연히 메서드 영역에 저장된 클래스만이 생성이 되어 적재된다. (너무 당연한 소리)
유의할 점은 힙 영역에 생성된 객체와 배열은 참조 타입으로서, JVM 스택 영역의 변수나 다른 객체의 필드에서 참조된다는 점이다. 즉, 힙의 참조 주소는 “스택”이 갖고 있고 해당 객체를 통해서만 힙 영역에 있는 인스턴스를 핸들링할 수 있는 것이다.
만일 참조하는 변수나 필드가 없다면 의미 없는 객체가 되기에 이것을 쓰레기로 취급하고 JVM은 쓰레기 수집기인 Garbage Collector를 실행시켜 쓰레기 객체를 힙 영역에서 자동으로 제거한다(메모리 회수).
이렇게 힙 영역은 GC(Garbage Collection)의 대상이 되는 공간이다.
그리고 효율적인 GC를 수행하기 위해 세부적으로 다음의 5가지 영역으로 나뉘게 된다.
이렇게 5가지 영역으로 나뉜 힙 영역은 다시 물리적으로 Young Generation, Old Generation으로 구분된다.
Young Generation : 생명 주기가 짧은 객체를 GC 대상으로 하는 영역
Eden : new를 통해 새로 생성된 객체가 위치. 정기적인 GC 후 살아남은 객체들은 Survivor로 이동.
Survivor 0 / Survivor 1 : 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 순차적으로 이동
Old Generation : 생명 주기가 긴 객체를 GC 대상으로 하는 영역. Young Generation에서 마지막까지 살아남은 객체가 이동

Java 스택 (Java Virtual Machine Stack)

스택 영역은 각 스레드별로 생성되는 독립적인 메모리 영역으로, 메서드 호출 시 생성되는 스택 프레임을 저장한다. int, long, boolean 등 기본 자료형을 저장하는 공간이다.
마지막에 들어온 값이 먼저 나가는 LIFO 구조로, push와 pop 기능 사용방식으로 동작한다.
메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성되고 메서드 안에서 사용되는 값들을 저장(Runtime Constant Pool에 대한 참조)하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다. 메서드 실행이 끝나면 프레임별로 제거된다.
예시로 Person p = new Person(); 와 같이 클래스를 생성할 경우, new에 의해 생성된 클래스는 힙 영역에 저장되고, 스택 영역에는 생성된 클래스의 참조인 p만 저장된다.
프로세스가 메모리에 로드될 때 스택 사이즈가 고정되어 있어, 런타임 시에 스택 사이즈를 바꿀 수는 없다. 만일 고정된 크기의 JVM 스택에서 프로그램 실행 중 메모리 크기가 충분하지 않다면(ex. 너무 깊은 재귀 호출 등), StackOverflowError가 발생할 수 있다.

PC(Program Counter) Register

PC Register는 각 스레드마다 별도로 존재하는 작은 메모리 공간으로, JVM이 현재 실행중인 명령어의 주소를 저장한다.
일반적으로 CPU에서의 Register는 연산에 필요한 정보를 저장하는 기억장치로 사용된다.
예시로 A와 B라는 피연산 값인 Operand, 이를 더하라는 연산 Instruction이 있다고 하자. A와 B, 덧셈 연산이 순차적으로 진행되는데, 이때 A를 받고 B를 받는 동안 이 값을 CPU가 어딘가에 기억해 둘 필요가 생긴다. 이 공간이 바로 CPU 내의 기억장치 Register이다.
하지만 자바의 PC Register는 다르다.
자바는 OS나 CPU 입장에서는 하나의 프로세스이기 때문에 JVM의 리소스를 활용해야 한다. 그래서 자바는 CPU에 직접 연산을 수행하도록 하는 것이 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며, 이를 위한 버퍼 공간으로 PC Register라는 메모리 영역을 만들게 된다.
따라서 JVM은 스택에서 비연산값 Operand를 뽑아 별도의 메모리 공간인 PC Register에 저장하는 방식을 취한다.
만약 스레드가 자바 메서드를 수행하고 있으면 JVM 명령(Instruction)의 주소를 PC Register에 저장한다. 그러다 만약 자바가 아닌 다른 언어(C, 어셈블리 등)의 메서드를 수행하고 있다면, undefined 상태가 된다.

네이티브 메서드 스택 (Native Method Stack)

네이티브 메서드 스택은 자바 코드가 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다. 또한, 자바 이외의 언어(C, C++, 어셈블리 등)로 작성된 네이티브 코드를 실행하기 위한 공간이기도 하다. 사용되는 메모리 영역으로는 일반적인 C 스택을 사용한다.
JIT 컴파일러에 의해 변환된 기계어가 여기에서 실행된다.

정리

JVM의 Runtime Data Area는 자바 프로그램의 실행을 위한 다양한 메모리 영역으로 구성된다. 각 영역은 고유한 역할을 수행하며, 프로그램의 효율적 실행과 메모리 관리, 다중 스레드 환경에서의 독립성을 보장한다.
메서드 영역은 클래스 로딩 및 메타데이터 관리를 담당하고,
힙 영역은 객체 생성을 위한 동적 메모리 할당 및 가비지 컬렉션의 대상이 되며,
Java 스택네이티브 메서드 스택은 각각 자바 메서드와 네이티브 코드 실행 시 필요한 호출 정보를 관리한다.
PC(프로그램 카운터)는 현재 실행 위치를 추적하여 명령어 흐름 제어에 핵심적 역할을 한다.

Reference