[Java] JVM

우노구나·2025년 7월 8일

java를 조금씩 깊게 공부를 하다보니 JVM의 역할과 구조를 정확하게 정리하는 단계가 필요하다고 느껴졌다.
학교 수업을 통해 대충 알기는 했지만 완전히 빠삭하게 내용을 숙지한 것 같지는 않아 이번 기회에 확실하게 잡고가겠다.


감사하게도 이미 잘 정리해주신 분이 계셔서 해당 글을 참고하여 공부 했다.

출처: https://hstory0208.tistory.com/entry/Java-JVM이란-구조와-특징에-대해-알아보자 [< Hyun / Log >:티스토리]

JVM(Java Virtual Machine)이란?


우리가 java 파일을 실행하려면 위와같은 과정을 거쳐야한다.
우선 java compiler로 java코드를 bytecode(.class file)로 변환한다.
변환하는 이유는 java파일을 윈도우, 맥, 리눅스 등 다양한 운영체제에서 돌아갈 수 있게하기 위해서이다.
이후 bytecode를 원하는 운영체제에 맞는 JVM을 이용하여 해당 운영체제가 이해할 수 있는 기계어로 변환한다.

추가로 c/c++의 경우에는 컴파일러가 기계어 코드로 변환해준다.

JVM의 구조

JVM의 구조는 다음과 같다.

GC(Garbage Collector)

GC는 힙 메모리 영역에 생성된 객체들 중 더 이상 참조되지 않는 객체를 자동으로 검색해 제거한다.

참고로 GC는 지금 객체가 계속 사용중인지 아닌지 체크도 하고 삭제도 해야하기 때문에 GC를 시작하기 전에 JVM내부의 모든 쓰레드를 멈춰 메모리 상태를 고정해두고 작업을 한다. 이후 작업이 끝난 뒤 쓰레드를 다시 실행.

이 때 쓰레드를 멈추기 때문에 프로그램이 순간적으로 멈추는데 이것을 Stop-The-World라고 한다.

아무래도 프로그램이 멈추는 것은 꽤나 중요한 사태(?)이기 때문에 이를 조절할 GC 튜닝이나 알고리즘 선택이 중요하다. (G1 GC, Parallel GC, CMS) 해당 내용은 추후에 제대로 정리할 계획이다.

Execution Engine(실행 엔진)

메모리(Method Area)에 올려진 바이트코드를 기계가 이해하는 형태로 변환하고 실행하는 역할을 수행한다.

Execution Engine 세부 역할로는

  1. Interpreter
    바이트코드(.class)를 한 줄씩 읽어서 실행.
    같은 코드를 실행할 때마다 바이크코드를 매번 해석해야해 속도가 느리다는 단점이 있다.

  2. JIT 컴파일러 (Just-In-Time Compiler)
    실행 중에 필요한 부분만 바이트코드를 기계어로 바꿔준다.
    인터프리터의 단점을 보완하기 위해 도입된 것으로 프로그램 실행 중에 바이트코드 전체 또는 일부를 네이티브 코드(CPU가 바로 실행할 수 있는 이진 코드)로 컴파일하고, 직접 실행한다.
    초기 컴파일에는 시간이 걸리지만, 한 번 컴파일된 코드는 매우 빠르게 실행된다.
    또한 JIT 컴파일러는 자주 실행되는 코드(핫스팟)를 분석해 우선적으로 컴파일하여 성능을 최적화한다.

동작 흐름을 요약하자면

.class 로딩 → 바이트코드 → 인터프리터 실행 시작
↓
반복 호출되는 코드 발견 (핫스팟)
↓
JIT 컴파일러가 Native Code 변환
↓
해당 부분 초고속 실행

Class Loader

.class 파일을 JVM으로 가져와서 메모리(Method Area)에 클래스의 메타정보(클래스 이름, 상속정보, 메서드 목록, 필드 정보)를 올리는 역할을 하는 시스템이다.
메모리에 올린 .class를 Execution Engine이 실행한다.

추가로 class loader는 필요할 때 동적으로 클레스를 메모리에 올리는 동적로딩을 담당한다.

Runtime Data Area

자바 프로그램 실행 중 JVM이 사용하는 메모리 영역 전체를 통틀어 Runtime Data Area라고 한다.

Runtime Data Area는 RAM에 포함되어 있다.

Runtime Data Area는 다음과 같이 이루어져 있다.

여기서 Method Area와 Heap은 JVM 전체가 공유하는 공유 영역이고
Stack, PC Register, Native Method Stack은 각각의 쓰레드 별로 존재하는 영역이다.

이제 Runtime Data Area의 구성요소를 하나씩 알아보자

Method Area

클래스 수준의 메타데이터 저장 영역으로 모든 스레드가 사용하는 공유 영역이다.

Method Area에 저장되는 내용은 다음과 같다

저장되는 내용설명
클래스 이름, 패키지명클래스의 완전한 이름 및 소속 패키지
상속 정보 (부모 클래스, 인터페이스)클래스의 상속 관계, 구현하는 인터페이스 목록
필드 정보 (멤버 변수)인스턴스 변수와 static 변수의 타입 및 이름
메서드 정보메서드의 시그니처(이름, 매개변수, 반환타입 등)
static 변수클래스 차원의 변수, 객체와 무관하게 Method Area에 저장
constant pool (상수 풀)리터럴, 상수, 심볼릭 참조(클래스명, 메서드명 등) 저장
final 변수 (컴파일 상수화된 값)변하지 않는 상수 값 저장
static 초기화 블록, static 메서드 구현부클래스 초기화 시 실행될 static 블록과 static 메서드 코드

Method Area에 클래스가 로드되는 과정은 다음과 같다.

1. Loading

.class 파일을 읽어옴

2. Linking

Verification: 바이트코드 검증

Preparation: static 변수 메모리 할당 (기본값으로)

Resolution: 심볼릭 참조 → 실제 주소 변환

3. Initialization

static 변수 값 초기화

static 블록 실행

참고로 이 모든 정보는 Method Area에 저장된다.

구체적인 예시

public class Example {
    static int x = 10;
    int y = 20;

    public void print() {
        System.out.println("Hello");
    }
}

JVM Method Area에 저장되는 정보:

클래스 이름: Example

상속 정보: Object를 상속

static 변수: x = 10

인스턴스 변수 정보: y

메서드 정보: print() 시그니처

constant pool: "Hello" 문자열 리터럴, 메서드 참조 심볼 등

TMI

JVM의 Method Area를 구현를 구현하는 방식으로 MetaspacePermGen이 있는데 PermGen은 Java8 이전, Metaspace는 Java8 이상의 버전에서 적용된다.

간략하게 설명하자면 PermGen은 JVM 내부 힙 메모리를 사용하고 크기가 고정되어있어서 크기가 부족할시 OOM(OutOfMemoryError)가 발생한다.
반면 Metaspace는 네이티브 메모리 (OS 메모리) 사용하고 크기를 유동적으로 조절이 가능하여 OOM이 발생할 확률이 적다.

Heap

heap은 프로그램 실행 중 생성되는 객체 인스턴스 저장 공간으로 new 키워드 또는 동적 객체 생성 시 메모리가 이 영역에 할당된다.
JVM의 Runtime Data Area 중 가장 큰 영역을 차지하고 Garbage Collection(GC)의 대상이 된다.

Heap의 영역 구조

JVM의 Heap은 위와 같이 Young Generation(Eden + S0 + S1)Old Genertaion(Tenured) 으로로 이루어져 있다.

Young Generation은 생성된지 얼마 안된 객체 저장되는 장소로 Eden spaceSurvivor Space(s0, s1)로 이루어져 있으며
Old Generation은 상대적으로 나이가 많은(?) 객체들이 저장되어있는 장소이다.
이 구조는 GC(Garbage Collection)와 밀접한 관련이 있는데

  1. 아직 GC가 발생하지 않은 새로 생성된 객체는 Eden에 저장된다.
  2. Eden이 가득차면 발생하는 Minor GC에서 살아남은 객체는 S0 또는 S1으로 이동한다.
  3. S0S1에 있는 객체는 Minor GC가 발생할 때마다 S0S1을 교차하면서 이동한다.
  4. Minor GC에서 일정 횟수 이상 살아남은 객체들은 마지막으로 Old Generation으로 승격(?)하게 된다. 이곳에서는 Major GC가 발생하고 상대적으로 GC의 빈도수가 낮다.

이와같은 과정으로 객체는 JVM Heap에 저장된다.

Stack

JVM의 Stack은 각 쓰레드마다 하나씩 존재하는 메모리 영역으로, 자바 프로그램 실행 시 메서드 호출에 필요한 정보를 저장하는 공간이다.

스택은 메서드 호출과 종료에 따라 프레임(Frame) 단위로 push/pop 되며, 함수 호출 스택(호출 스택) 역할을 한다.

특징요약

항목설명
단위Stack Frame (메서드 호출 1회당 1개 생성)
저장 정보로컬 변수, 피연산자 스택, 메서드 정보, 예외 핸들링 정보 등
생성 시점스레드가 시작될 때 생성됨
소멸 시점스레드가 종료될 때 소멸됨
접근 방식LIFO (Last In First Out)
GC 대상 여부❌ (GC가 관리하지 않음. 자동 소멸)
오류 종류StackOverflowError, OutOfMemoryError

Stack Frame 구성

메서드가 호출되면 JVM Stack에 Stack Frame이 하나 push되고, 메서드가 끝나면 pop된다.
각 Stack Frame은 다음과 같이 구성된다:

Local Variable Array (로컬 변수 배열)

메서드의 매개변수와 지역변수가 저장됨

int, float, 참조형 등 포함됨

Operand Stack (피연산자 스택)

명령어가 실행될 때 사용되는 피연산자 저장소

예: iadd 연산은 피연산자 스택의 두 정수를 꺼내 덧셈 후 다시 push

Frame Data (프레임 정보)

상위 호출자와의 연결 정보

예외 처리 테이블

리턴 주소 등

코드 예시

public class Example {
    public static void main(String[] args) {
        int result = add(2, 3);
    }

    public static int add(int a, int b) {
        return a + b;
    }
}
  1. main()이 호출되면 Stack Frame 생성 → JVM Stack에 push

  2. add(2, 3) 호출되면 새로운 Stack Frame 생성 → push

  3. ab는 Local Variable Array에 저장

  4. a + b 연산은 Operand Stack에서 수행

  5. 결과 return → add()의 Frame pop → main()으로 돌아감

  6. 프로그램 종료 → JVM Stack 사라짐


JVM Stack 관련 에러

StackOverflowError
무한 재귀 호출 시 발생

public void infinite() {
    infinite(); // 계속 Stack Frame이 쌓이다가 터짐
}

OutOfMemoryError: StackOverflowError
전체 메모리가 부족하여 스택 생성조차 못할 때 발생

PC Register

JVM의 PC Register는 각 스레드가 실행 중인 명령어의 주소(위치)를 저장하는 레지스터이다.
자바 바이트코드에서 어떤 명령어를 현재 실행 중인지, 그리고 다음에 무엇을 실행할 것인지를 결정하는 데 사용된다.

특징요약

항목설명
역할현재 실행 중인 JVM 명령어의 주소를 저장
단위바이트코드 명령어의 주소(오프셋)
스레드별 존재스레드마다 별도로 존재 (Thread-private)
메모리 크기매우 작음 (주소값 하나만 저장하므로)
GC 대상 여부❌ GC 관리 대상 아님
예외 발생 시 동작예외 처리를 위해 적절한 명령어 위치로 PC 레지스터가 이동

역할과 동작

JVM은 자바 코드를 클래스 파일로 컴파일한 후, 바이트코드 명령어를 실행하는데, 이때 PC Register는 이 바이트코드의 현재 위치를 기억하는 역할을 한다.

TMI(JVM 레지스터와 CPU 레지스터의 차이)

구분JVM의 PC RegisterC언어에서의 레지스터 (예: eax, ecx, rip, etc.)
속한 대상JVM (Java Virtual Machine)실제 CPU 하드웨어
용도현재 실행 중인 바이트코드의 주소(위치) 추적명령어 실행, 데이터 저장, 주소 계산 등 다양한 역할
사용 방식바이트코드 해석을 위한 내부 주소 추적용기계어 명령을 실행하기 위한 레지스터 수준 연산
크기매우 작고 JVM 내부에서만 사용보통 32비트/64비트 하드웨어 레지스터
예시JVM 내부에서 iload, iadd 등의 명령 위치 추적RIP, EAX, RCX, ESP 등 CPU의 물리 레지스터

Native Method Stack

자바 코드에서 네이티브(native) 메서드를 호출할 때 사용하는 스택이다.

자바에서는 JNI (Java Native Interface)를 통해 C/C++ 같은 네이티브 언어로 구현된 메서드를 호출할 수 있는데, 이때 JVM이 바이트코드가 아닌 기계어(native code)를 실행해야 하므로, 자바 스택이 아닌 별도의 스택이 필요한데 그게 바로 Native Method Stack이다.

예시

public class Example {
    public native void nativeMethod(); // C/C++로 구현됨
    static {
        System.loadLibrary("nativeLib"); // 네이티브 라이브러리 로드
    }
}

Native Method Stack vs JVM Stack

구분JVM StackNative Method Stack
실행 대상자바 메서드 (바이트코드)네이티브 메서드 (C, C++ 등)
언어자바C, C++ (JNI)
메모리 구조JVM에 의해 관리되는 스택 프레임 구조운영체제에 따라 다름 (일반적인 C 함수 스택 구조)
호출 방법메서드 호출 시 Stack Frame 생성JNI를 통해 OS 함수 호출 방식 사용

내부 동작 구조

  1. 자바 코드에서 native 메서드를 호출
  2. JVM이 Native Method Stack을 통해 OS/라이브러리 함수로 연결
  3. OS 스택에서 C/C++ 함수 실행 → 연산 수행
  4. 다시 JVM으로 복귀 → 결과 반환
profile
기술 블로그

0개의 댓글