Study

JVM의 구조와 동작 원리

kanado 2025. 1. 23. 17:50

자바 개발자라면 꼭 알아야 할 JVM(Java Virtual Machine)의 구조와 동작 원리에 대해 정리해보았다. 자바 프로그램이 어떻게 실행되는지, JVM 내부에서 어떤 일이 일어나는지를 이해하면, 더 최적화된 코드 작성과 문제 해결에 큰 도움이 될 것이라고 생각한다.

 

1. JVM (Java Virtual Machine)

  • 3가지 핵심 구성 (Class Loader, Runtime Data Area, Execution Engine)
  1. Class Loader: java 컴파일을 통해 동적 로딩(Dynamic loading)으로 프로그램 실행 중에 필요한 시점에 해당 클래스의 바이트 코드만 실시간으로 Runtime Data Area에 적재한다.

동적 로딩 (Dynamic Loading): 프로그램 실행 시 필요한 클래스나 리소스를 미리 로드하지 않고, 필요할 때 실시간으로 로드하여 메모리에 적재하는 방식. 이는 정적 로딩(Static Loading)과 대비되는 개념으로, 정적 로딩은 프로그램 실행 전에 모든 필요한 리소스와 클래스를 메모리에 미리 로드하는 것을 말한다.

  • Runtime Data Area에 적재하는데 3가지 단계( 동적 로딩의 동작 과정 )
    1. 로딩: 클래스 파일.class을 찾아서 읽고, 해당 클래스의 바이트코드를 메모리의 (Runtime Data Area) Method Area에 적재.
    2. 링크: 로딩된 .class 파일은 JVM에서 실행할 수 있도록 검증(안전한지), 준비(변수와 기본값을 위한 메모리), 해석(실제 메모리 주소로 변환) 과정을 거쳐 실행될 준비 완료.
    3. 초기화: 클래스 및 정적 코드 블록(바로 실행해도 문제없는)을 실행합니다.

    2. Runtime Data Area: JVM이 프로그램을 실행하기 위해 사용하는 메모리 영역으로 해당 영역은 프로그램 실행 중 생성되는 다양한 데이터를 저장하고 관리하는데 사용된다.

  • 5가지 구성 요소
    1. PC Register: 각 스레드가 어떤 부분(코드)을 어떤 명령(CPU)으로 실행해야 할지 관리하고 기록(현재 실행 중인 명령의 주소를 저장)
    2. JVM Stack (JVM 스택 영역): JVM 스택은 스레드마다 고유하게 존재하는 영역입니다. 각종 메서드 실행 상태와 지역 변수를 저장.
    3. Native Method Area: 네이티브 코드(C, C++ 등 비-Java 코드) 실행 시 사용.
    4. Heap (힙 영역): 힙은 JVM 메모리가 관리하는 부분 중 가장 큰 부분으로 모든 스레드가 공유하며 자바 애플리케이션이 생성한 모든 객체와 배열이 저장됩니다. (힙 영역에서 사용되지 않는 객체는 GC(가비지 컬렉터)에 의해 제거되어 메모리 공간을 확보)
    5. Heap의 String 영역 (String Pool): java에서는 문자열을 효율적으로 관리하기 위해 Heap 내에 String Pool이라는 특별한 공간을 제공한다. String Pool은 중복된 문자열 객체를 제거하고 메모리 사용을 최적화하기 위해 사용된다. String Pool에 저장된 문자열은 불변이다. 즉, 한 번 생성되면 변경할 수 없다. 하지만 new String()을 사용하면 문자열이 String Pool에 저장되지 않고 Heap의 일반 영역에 새로운 객체로 생성되면서 중복성을 확인하지 않아 메모리 낭비 하게된다.
    6. Method Area (메소드 영역): 프로그램 실행에 필요한 클래스, 인터페이스 와 메타데이터(클래스 구조, 메서드, 정적(static)변수, 상수 등)를 바이트 코드로 저장. 또한 클래스는 JVM이 실행 중일 때 최초로 참조될 때 한 번만 로드되며, 이후에는 Method Area에 저장된 내용을 사용.Runtime Data Area: JVM이 프로그램을 실행하기 위해 사용하는 메모리 영역으로 해당 영역은 프로그램 실행 중 생성되는 다양한 데이터를 저장하고 관리하는데 사용된다.
Method
Area
클래스 구조 및 메타데이터 저장 클래스 메타데이터, 메서드 메타데이터, 상수,
정적(static)변수
모든 스레드가 공유 JVM이 종료될 때까지 유지
Heap 객체와 배열 저장 객체, 배열, 객체 내부의 멤버 변수 모든 스레드가 공유 Garbage Collector가 관리
Stack 메서드 호출과 지역 변수 관리 지역 변수, 메서드 호출 정보, 참조 변수 스레드별로 독립 메서드 종료 시 자동 해제
클래스 메타데이터: 클래스 이름, 해당 클래스의 부모 클래스 이름, 구현한 인터페이스 목록,클래스가 abstract인지, final인지 등의 접근 제한자 정보


 3. Execution Engine: 메모리에 로드된 바이트코드를 실제 기계어로 OS에 맞게 변환하고 Runtime Data Area에서 필요한 데이터를 가져와 실행한다.

 - 3가지 구성 요소

  1. 인터프리터(Interpreter): 인터프리터는 바이트코드를 한 줄씩 읽어서 기계어로 변환하고 실행한다. but 같은 코드를 반복해서 실행할 때마다 매번 다시 변환해야 하므로 실행 속도가 느린 성능 문제 존재했으나, JIT 컴파일러의 도입으로 많은 최적화가 이루어졌다.
  2. Just-In-Time (JIT) 컴파일러: 실행 중인 애플리케이션의 반복적으로 실행되는 부분을 감지하여, 그 부분만 기계어로 미리 컴파일한다. 이렇게 미리 컴파일된 코드는 고속으로 실행할 수 있으며, 프로그램의 전체 실행 속도를 향상.
  3. 가비지 컬렉터(Garbage Collector): 힙 메모리 영역에서 더 이상 참조되지 않는 객체들을 자동으로 검출하고 제거한다. 이 과정은 메모리를 효율적으로 관리하며, 메모리 누수와 같은 문제를 방지한다.
메모리 누수(Memory Leak): 프로그램이 동작하면서 더 이상 필요하지 않은 메모리(사용하지 않는 객체)가 해제되지 않고 계속 메모리 공간을 차지하는 상태

 

2. JVM에서 실행되는 큰그림 순서대로 정리

  1. 소스 코드 작성 (example.java)
  2. 컴파일 (javac) → 바이트코드 생성 (example.class)
  3. JVM 실행 시작
  4. Class Loader
    • 클래스 로드 (Method Area에 메타데이터 저장)
  5. Runtime Data Area
    • Method Area: 클래스 정보, 정적 변수
    • Heap: 객체, 배열 저장
    • Stack: 지역 변수, 메서드 호출 정보
  6. Execution Engine
    • 바이트코드 해석 및 실행 (Interpreter & JIT Compiler)
    • 필요 시 Garbage Collector가 메모리 관리

 

3. Execution Engine과 Runtime Data Area가 동시에 작동하는 방식

  1. 메서드 호출
    • JVM이 특정 메서드를 호출하면, Execution Engine은 해당 메서드의 바이트코드를 Method Area에서 가져온다.
    • 메서드 호출에 필요한 매개변수는 Stack에 저장된다.
  2. 객체 생성
    • 객체를 생성하면 Execution Engine은 Heap 영역에 객체를 생성한다.
    • 객체 참조는 Stack에 저장된다.
  3. 변수 연산
    • 메서드가 실행되면서, 지역 변수는 Stack에 저장된다.
    • 필요한 데이터는 Heap이나 Method Area에서 읽어오고, 연산 결과를 다시 저장한다.
  4. Garbage Collection
    • Execution Engine은 Heap 영역의 객체 중 참조되지 않는 객체를 Garbage Collector에 의해 제거하도록 관리한다.