JVM 내부 구조

JVM은 자바 바이트코드를 실행시키는 가상 컴퓨터로,

자바의 "한 번 작성하면 어디서나 실행된다"는 특징을 실현합니다.

 

 

 

주요 구성 요소

 

 

Class Loader SubSystem

클래스 로더는 자바의 런타임 중에 필요한 클래스와 인터페이스를 동적으로 로드합니다. 

이 과정을 크게 3단계 로딩(Loading), 링킹(Linking), 초기화(Initialization)로 나눌 수 있습니다.

 

로딩(Loading) : .JVM(Java Virtual Machine)의 클래스 로딩 과정에서 매우 중요한 첫 번째 단계입니다. 이 단계에서는 자바 프로그램이 실행되면서 필요한 클래스 파일들을 메모리로 로드하는 과정이 이루어집니다.

  • 바이트코드 읽기: 클래스 로더는 .class 파일에서 바이트코드를 읽어들입니다. 이 파일은 자바 소스 파일(.java)이 컴파일되어 생성된 것입니다.
  • 메모리 할당: 읽어들인 바이트코드는 메모리에 할당됩니다. JVM 내의 메소드 영역(Method Area)에 클래스 레벨의 정보(클래스 이름, 부모 클래스, 메소드, 변수 등)가 저장됩니다.
  • 클래스 객체 생성: 로드된 클래스 정보를 바탕으로 JVM은 해당 클래스의 Class 객체를 생성합니다. 이 객체는 힙(Heap) 영역에 저장되며, 자바 리플렉션(Reflection)을 통해 프로그램에서 사용될 수 있습니다.

 

링킹(Linking) : 검증(Verification), 준비(Preparation), 분석(Resolution)과정을 통해 클래스가 제대로 로드되었는지 확인하고, 클래스 변수를 초기화합니다. 

  • 검증(Verification): 로드된 .class 파일의 바이트코드가 JVM의 명세와 일치하는지 검증합니다. 이는 안전성을 확보하기 위한 단계로, 코드가 적절한 형식을 가지고 있으며, 안전하게 실행될 수 있는지 확인합니다. 검증 실패 시, VerifyError가 발생합니다.
  • 준비(Preparation): 클래스 또는 인터페이스에 필요한 메모리를 할당한다.이 과정에서 정적 필드(static fields)가 생성되고, 기본값(null, 0, false 등)으로 초기화한다.
  • 분석(Resolution): 클래스, 인터페이스, 필드, 메소드에 대한 심볼릭 메모리 참조를 직접 참조로 변환한다. 즉, 클래스 내의 모든 심볼릭 이름을 실제 메모리 주소나 참조로 매핑하는 과정입니다.

 

초기화(Initialization) : 클래스와 인터페이스에 대한 모든 정적 변수를 기본값이 아닌 명시적으로 선언된 값으로 설정하고, 정적 블록(static blocks)이 실행되는 단계입니다. 클래스가 실제로 사용되기 전에 실행되며, 다음과 같은 경우에 발생합니다:

  • 클래스의 인스턴스가 생성될 때
  • 클래스의 정적 메소드가 호출될 때
  • 클래스의 정적 필드가 사용되거나 할당될 때 (final 필드 제외)
  • 자바 리플렉션을 사용하여 클래스가 직접 로드될 때

초기화 과정은 클래스 로더에 의해 동기화되어, 여러 스레드가 동시에 같은 클래스를 초기화하지 못하도록 합니다. 클래스의 부모가 아직 초기화되지 않았다면, 부모 클래스가 먼저 초기화되는 순서로 진행됩니다.

 

클래스 로더의 종류

자바는 다양한 종류의 클래스 로더를 제공합니다:

  • Bootstrap Class Loader : JVM의 가장 상위 클래스 로더로, 자바의 핵심 클래스들(java.lang.* 패키지 등)을 로드합니다.
  • 확장 클래스 로더(Extension Class Loader): 표준 라이브러리 확장 클래스들을 로드합니다. 예를 들어, JAVA_HOME/lib/ext 폴더나 다른 지정된 경로에 있는 클래스들이 여기에 해당됩니다.
  • 시스템 클래스 로더(System Class Loader): 애플리케이션 클래스패스에서 클래스를 로드합니다. 사용자가 개발한 클래스나 서드 파티 라이브러리 클래스가 이에 해당됩니다.

 

Runtime Data Area

JVM 메모리는 실행 중인 프로그램의 다양한 데이터를 저장하기 위한 영역으로 구분된다. 

메소드 영역(Method Area), 힙(Heap), 스택(Stacks), 프로그램 카운터(PC Register), 네이티브 메서드 스택(Native Method Stack)으로 나눈다. 

  1. 메소드 영역(Method Area): 클래스 수준의 정보(클래스 이름, 구조, 상수, 정적 변수, 메소드 및 생성자의 코드 등)를 저장합니다. 모든 스레드에 의해 공유되며 JVM 시작 시 생성됩니다.
  2. 힙 영역(Heap Area): 모든 클래스 인스턴스와 배열의 메모리가 할당되는 런타임 데이터 영역입니다. 또한 모든 스레드에 의해 공유되며, 객체에 대한 메모리 할당 및 해제를 관리하는 가비지 컬렉션이 이루어지는 곳입니다.
  3. 자바 스택(Java Stacks): 각 자바 스레드는 자신만의 스택을 가지며, 스레드가 생성될 때 함께 생성됩니다. 스택은 메소드 호출과 지역 변수를 저장하고 메소드 호출 및 반환 과정에서 중요한 역할을 합니다.
  4. 프로그램 카운터 레지스터(PC Register): 현재 실행 중인 JVM 명령어의 주소를 포함합니다. 각 스레드는 현재 실행 중인 명령어의 주소를 저장하기 위한 자신만의 PC 레지스터를 가집니다.
  5. 네이티브 메소드 스택(Native Method Stacks): 자바가 아닌 네이티브 메소드 호출을 위해 예약된 스택입니다.

JVM 단위에 속하는 힙과 메서드 영역은 JVM이 시작될 때 생성되고, JVM이 종료될 때 소멸되며, JVM 하나에 힙 하나, 메서드 영역도 하나가 생성 된다.

스레드 단위에 속하는 PC 레지스터, JVM 스택, 네이티브 메서드 스택도 스레드가 생성/소멸될 때 함께 생성/소멸되며, 스레드 하나에 PC 레지스터, JVM 스택, 네이티브 메서드 스택도 각 하나씩 생성된다.

 

 

Execution Engine(실행 엔진)

실행 엔진은 JVM 내에서 바이트코드를 실행하는 핵심 구성 요소입니다. 바이트코드는 자바 소스 코드가 컴파일된 형태로, 실행 엔진은 이 바이트코드를 실제 CPU가 이해할 수 있는 기계어로 변환하고 실행하는 역할을 합니다. 실행 엔진의 주요 구성 요소로는 다음과 같은 것들이 있습니다.

 

  • 인터프리터(Interpreter): 바이트코드를 한 줄씩 읽어서 실행합니다. 이 방식은 비교적 느린 실행 속도를 가지지만, 프로그램의 초기 실행 단계에서 빠르게 시작할 수 있게 해줍니다.
  • JIT 컴파일러(Just-In-Time Compiler): 실행 속도를 개선하기 위해, 자주 사용되는 바이트코드를 미리 컴파일하여 네이티브 코드로 변환하고 캐싱합니다. 이를 통해 반복되는 코드의 실행 속도를 크게 향상시킬 수 있습니다.
  • 가비지 컬렉터(Garbage Collector): 동적으로 할당된 메모리 영역(힙)에서 더 이상 사용되지 않는 객체를 자동으로 검출하고 해제하여, 메모리를 효율적으로 관리합니다.

정리

JVM(Java Virtual Machine)의 3개 주요 구성 요소는 각각 중요한 역할을 수행하여 자바 애플리케이션의 실행을 가능하게 합니다. 앞서 말했던 구성 요소들의 역할을 간단하게 정리하고 마치려고 합니다. 

  1. 클래스 로더 시스템(Class Loader System):
    • 자바 애플리케이션을 실행할 때 필요한 클래스와 인터페이스를 로드합니다.
    • .java 파일에서 컴파일된 바이트코드(.class 파일)를 메모리에 로드하고, 이를 실행할 준비를 합니다.
  2. 런타임 데이터 영역(Runtime Data Area):
    • 자바 애플리케이션을 실행하기 위해 필요한 다양한 데이터(클래스 정보, 필드, 메소드, 스레드 정보 등)를 저장하는 메모리 영역입니다.
    • 힙(Heap), 메소드 영역(Method Area), 스택(Stacks), PC 레지스터, 네이티브 메소드 스택 등으로 구성되어 애플리케이션의 동적 데이터를 관리합니다.
  3. 실행 엔진(Execution Engine):
    • 로드된 클래스의 바이트코드를 실행합니다.
    • 인터프리터 방식으로 명령어를 하나씩 실행하거나, JIT 컴파일러를 사용하여 효율적으로 실행 속도를 향상시킵니다.
    • 가비지 컬렉터를 통해 사용되지 않는 메모리를 정리하고, 네이티브 메소드 인터페이스(Native Method Interface)를 통해 네이티브 코드와 상호 작용합니다.

이 3개의 구성 요소는 함께 작동하여, 자바 애플리케이션을 효율적이고 안정적으로 실행할 수 있는 환경을 제공합니다.

 


Reference

 

Back to the Essence - Java 컴파일에서 실행까지 - (2)

Back to the Essence - Java 컴파일에서 실행까지 - (2)Java 11 JVM 스펙을 기준으로 Java 소스 코드가 어떻게 컴파일되고 실행되는지 살짝 깊게 알아보자. 이번엔 2탄 실행 편이다. 1탄 컴파일 편은 여기에..

homoefficio.github.io

 

'Java' 카테고리의 다른 글

인터페이스와 추상 클래스 비교  (0) 2025.02.03
인터페이스에 대한 생각 정리  (0) 2025.02.03