Post

JVM 내부 동작 원리

jdk.png

JVM (Java Virtual Machine)

  • Write once, Run everyWhere
  • Java 프로그램 실행을 담당하는 가상의 컴퓨터로, Java 바이트 코드 실행, 메모리 관리, 가비지 컬렉션 등을 담당한다.
  • 자바와 운영체제 사이에서 중계자 역할을 하며, 자바가 운영체제에 구애받지 않고 프로그램을 실행할 수 있도록 도와준다.

JRE (Java Runtime Environment)

  • Java 프로그램 실행을 위한 환경을 제공하며, JVM과 Java 클래스 라이브러리가 포함된다.
  • JRE만 있으면 Java 프로그램을 실행할 수 있지만, Java 프로그램을 개발하기 위해서는 JDK가 필요하다.
  • JDK 를 설치하면 JRE도 같이 설치된다. (JDK = JRE + a)

JDK (Java Development Kit)

  • Java 애플리케이션을 개발하기 위한 도구 모음으로 JRE와 함께, 컴파일러, 디버거, 기타 개발 도구가 포함된다.
  • Java 프로그램을 개발, 컴파일, 실행할 수 있는 모든 것을 제공한다.
  • JDK를 이용하여 Java 코드를 작성하고, 이를 실행하기 위해 JRE를 사용한다.


JVM 특징

  • 컴파일된 바이트 코드를 기계어로 변환

  • 스택 기반의 가상 머신

  • 메모리 관리와 GC 수행


예로 C는 바로 기계어로 컴파일하기 때문에, HW 기종에 맞게 각각 컴파일 되어야 한다.

반면, 자바 프로그램은 중간 단계 언어( *.class )로 컴파일 하여 JVM만 각 OS에 설치되어 있다면 HW 기종에 관계 없이 한 번만 컴파일 하면 된다.

바이너리 코드/ 바이트 코드/ 기계어

바이너리 코드

  • 컴퓨터가 인식할 수 있는 0과 1로 구성된 이진코드
  • 모든 바이너리 코드가 기계어는 아니다.

바이트 코드

  • JVM 이 이해할 수 있는 중간 코드
  • 고급 언어로 작성된 코드가 컴파일되어 생성된다.
  • 플랫폼에 독립적이며, 어떤 플랫폼에서든 JVM이 있는 한 실행할 수 있다.
  • JIT(Just-in-time)컴파일러 또는 인터프리터에 의해 네이티브 코드로 변환되어 실행된다.

기계어

  • CPU가 직접 해독하고 실행할 수 있는 특정한 바이너리 코드
  • 특정 CPU 아키텍처에 종속적이며, CPU 제조사가 정의한 명령어 집합에 따라 달라진다.
  • CPU가 변경되면 기계어가 달라진다. (같은 동작을 하는 명령어지만 완전히 다른 0과 1이 나열될 수도 있다.)
  • 모든 기계어가 이진코드 인 것은 아니며, 기계어는 특정 언어가 아니다.

즉, 소스 파일(.java)은 자바 클래스 파일(.clss)로 컴파일 된다.

JVM이 클래스파일(*.class)을 OS에 맞는 기계어로 변환(인터프리터와 JIT 컴파일러를 통해)하여 실행된다.




Java 실행 방식

compile.png

1. 자바 컴파일
  • 자바 소스 파일이 컴파일러에 의해 바이트코드 파일로 변환된다.
2. 클래스 로딩
  • JVM은 Class Loader를 통해 클래스 파일을 메모리의 Runtime Data Area의 메서드 영역에 로드한다.
3. 클래스 검증
  • 로드된 클래스 파일이 자바 언어 명세에 따라 유효한지 검증한다.
4. 메모리 할당 및 초기화
  • JVM은 클래스의 변수를 위한 메모리를 할당하고 그 변수를 기본값으로 초기화한다.
  • 해당 클래스의 정적 변수와 정적 메서드를 위한 메모리를 메서드 영역에 할당한다.
  • 이 메모리는 클래스가 로드될 때 생성되며, 각 클래스에 대한 메타데이터와 함께 저장된다.
5. JVM 은 execution engine 통한 실행
  • JVM은 Execution Engine을 통해 클래스 파일의 바이트 코드를 해석하여 기계어로 변환하고 해석한다.
  • 이 과정에서 JIT 컴파일러 사용하여 성능을 향상시킨다.
    • JIT 컴파일러는 프로그램을 실행 하는 동안 반복되는 코드 블록을 식별하고 이를 네이티브 기계 코드로 컴파일하여 실행 속도를 향상시킨다
    • execution engine : Loading 된 클래스의 바이트코드를 해석 (바이트->바이너리)
6. GC을 수행
  • JVM은 더 이상 필요하지 않은 객체를 감지하고 자동으로 메모리에서 해제한다.
7. 자원 해제 및 종료
  • 프로그램이 종료되거나 JVM이 종료될 때, JVM은 사용한 리소스를 해제하고 종료한다.




JVM 구성요소

jvm.png

JVM은 크게 아래과 같이 구성되어 있다.

  • Class Loader
  • Runtime Data Area
    • PC register
    • Stack Area
    • Native Method Stack
    • Heap Area
    • Method Area
  • Execution Engine
    • Interpreter
    • JIT Compiler
    • Garbage Collector


Class Loader System

classloader.png 자바 클래스 파일(.class)을 실행 시점(Runtime)에 읽어 메모리(Runtime Data Area)에 로드하고 실행하기 위해 사용하는 메커니즘

클래스 로더는 JVM의 일부분으로, JVM이 실행되는 동안 메모리 내에 존재한다.

자바 프로그램의 실행 중 필요한 클래스를 동적으로 로드하고, 연결하고 초기화하는 역할을 한다. 자바의 클래스들은 한 번에 모든 클래스가 메모리에 올라가지 않고, 필요할 때 메모리에 올라간다.

로딩 -> 링크 -> 초기화


로딩

클래스 로더는 클래스 패스에서 (.claass)파일을 찾아 메모리에 로드한다.
이때 클래스 로더는 .class 파일을 바이너리 형식으로 읽어, JVM의 메모리 영역인 메서드 영역에 저장한다.

로딩이 왼료되면, 해당 클래스 타입의 Class 객체가 생성되어 Heap 영역에 저장된다.

이 클래스 객체는 해당 클래스에 대한 메타데이터(클래스, 인터페이스, Enum 등)와 정보를 포함한다.

한번에 메모리에 모두 로드하지 않고, 필요한 경우 동적으로 메모리에 로드한다.

1. Bootstrap Class Loader

JVM의 핵식 클래스 로더로 rt.jar와 같은 핵심 자바 API를 로드한다.
네이티브 코드로 구현되어 있으며, Java로 접근할 수 없고 가장 높은 우선순위를 갖는다.

2. Extension Class Loader

lib/ext 디렉터리나 java.ext.dirs 시스템 속성에 지정된 위치에서 확장 클래스들을 로드한다.
부트스트랩 클래스 로더 다음의 계층에 위치하며, 사용자 정의 클래스 로더보다 우선적으로 사용한다.

3. Application Class Loader

애플리케이션 클래스패스(classpath)에 지정된 클래스, 애플리케이션에서 작성한 클래스와 라이브러리를 로드한다. 자바 응용 프로그램의 기본 클래스 로더로, 대부분의 사용자 클래스와 라이브러리를 로드하는데 사용된다.


링크

로드된 클래스 파일을 JVM이 사용할 수 있도록 준비

  • Verify -> Prepare -> Resolve

1. Verification

.class 파일 형식이 유효한지 검사한다.
로드된 클래스가 자바 언어 명세에 부합한지 확인하고, 유효하지 않은 경우 런타임 에러(java.lang.VerifyError)가 발생 한다.

2. Preparation

클래스 변수(static 변수)와 같은 메모리를 할당하고 초기화한다.

3. Resolution

클래스의 심볼릭 메모리 레퍼런스를 메모리 영역에 존재하는 실제 메모리 주소로 변경한다.

심볼릭 메모리 레퍼런스

클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아닌 참조 대상의 이름만 갖는 것
즉, 실제 메모리 주소가 아니라 이름만 갖는다.


Initialization

클래스 변수의 초기화 코드가 실행된다. static 초기화 블록이 실행되며, 변수가 초기화된다.
링크의 prepare 단계에서 확보한 메모리 영역에 클래스 변수들 (= static 변수)를 적절한 값으로 초기화한다.

Using

초기화된 클래스는 이제 JVM에서 사용될 준비가 되어 있으며, 인스턴스 생성이나 메서드 호출과 같은 작업이 가능하다.

Unloading

클래스 로더가 클래스를 메모리에서 제거한다. JVM이 필요하지 않은 클래스를 GC를 통해 언로드한다.




Runtime Data Area

runtimedata.png

JVM이 프로세스로 수행되기 위해 OS로 부터 할당받는 메모리 영역으로 데이터 저장, 스레드 관리, 메서드 실행, 객체 관리 등을 수행한다.

프로그램 실행 시, 메서드 호출에 따라 스택 프레임이 생성되고, 메서드 호출이 완료되면 해당 스택 프레임이 제거된다. 객체는 힙에 동적으로 생성되며, 가비지 컬렉션을 통해 메모리를 관리한다.

클래스는 처음 참조될 때 메서드 영역에 로드되며, 이후 해당 클래스 정보는 메서드 영역에서 재사용 된다.

Method Area

  • JVM이 시작될 때 생성되며, 모든 스레드가 공유하는 영역
  • 여기에 스태틱 영역(정적 변수들이 저장되는 공간)이 포함된다.
  • 클래스정보, 변수정보, Method, static 변수, 상수풀 등이 저장된다.


Heap Area

  • 모든 스레드에 공유되며, new 명령어로 생성된 인스턴스와 객체가 저장되는 영역
  • GC의 대상이 된다.
  • 힙은 JVM에서 가장 큰 메모리 영역으로 모든 스레드가 공유한다.
  • GC가 주기적으로 힙 메모리를 청소하여 사용되지 않는 객체를 제거한다.


PC Register

  • 스레드가 현재 실행 중인 명령어의 주소를 추적하며, 각 스레드는 독립적인 PC 레지스터를 갖는다.
  • 현재 실행 중인 자바 바이트코드 명령어의 주소를 저장하고, 이 주소를 사용하여 다음에 실행할 명령어를 결정한다.
  • 메서드 호출 및 명령어 실행 후, PC 레지스터의 값을 업데이트하여 다음 명령어의 주소를 가리킨다.
  • JVM은 스택 기반의 가상 머신으로, 모든 명령어 실행은 스택 프레임을 통해 이루어진다.
  • JVM은 메서드가 호출될 때마다 새로운 스택 프레임을 생성하고, 해당 프레임에 PC 레지스터 값을 업데이트하여 각 메서드의 실행 흐름을 관리한다.
    메섣드 호출이 완료되면 스택 프레임이 제거되고, PC 레지스터는 이전 호출로 돌아간다.

CPU는 명령어를 실행하고 제어하는 장치로, 주기억장치(메모리)와 상호작용하여 데이터를 처리한다.
명령어는 CPU 내부의 레지스터와 캐시메모리를 통해 빠르게 처리된다.

자바는 JVM이라는 가상 머신에서 실행된다. 따라서 자바 프로그램은 OS나 CPU에서 직접 실행되지 않고, JVM이 중간에서 처리한다.

PC 레지스터는 CPU가 현재 실행 중인 명령어의 주소를 저장하는 레지스터로, JVM의 각 스레드는 자신의니 PC 레지스터를 각ㅈ는다.
명령어는 스택 프레임의 Operands 영역에 저장되며, JVM은 이 영역에서 명령어를 가져와 PC 레지스터에 주소를 저장한 후 , CPU에게 실행할 명령어를 전달한다.

CPU는 PC레지스터의 주소를 참조하여 해당 명령어를 메모리에서 읽어 실행한다.
명령어 실행이 끝나면, PC 레지스터의 값은 다음 실행할 명령어의 주소로 업데이트 된다.

위의 과정은 스레드가 메서드 호출 시다마 반복된다.


Stack Area

  • 각 스레드마다 하나씩 생성
  • 호출된 메소드의 파라미터, 지역 변수, 리턴 값 및 연산 값 등이 저장되는 영역
  • 메소드가 호출될때마다 스택 프레임이라 불리는 블럭이 하나씩 생성되고 메서드 실행이 종료되면 삭제된다.
  • 스택 오버플로우 발생시 JVM은 해당 스레드에 대해 예외를 발생시킨다.
  • LIFO


구성요소

스택 프레임
각 메서드 호출마다 스택 프레임이 생성되며, 메서드 실행이 끝나면 제거된다.
각 스택 프레임은 메서드 호출에 대한 정보를 담는다.

지역 변수 배열
메서드에서 선언된 지역 변수와 매개 변수 저장

오퍼랜드 스택
메서드 실행 중에 연산에 필요한 값을 저장하는 공간

프레임 데이터
메서드의 실행 상태 저장(메서드 호출자, 리턴 주소 등)


Native Method Stack

  • 각 스레드마다 하나씩 생성
  • Java 이외의 언어로 작성된 메소드의 정보가 저장되는 공간
  • Kernel 이 자체적으로 Stack 을 잡아 독자적으로 프로그램을 실행시키는 영역
  • JNI 표준 규약 제공
    JNI

    자바와 다른 프로그래밍 언어 간의 상호작용을 위한 표준 규약
    자바 프로그램이 Native 메서드를 호출할 수 있다.
    자바 애플리케이션이 운영체제의 특정 기능이나 외부 라이브러리와 연동할 수 있도록 한다.




Execution Engine

engine.png

클래스로더를 통해 Runtime Data Area에 적재된 자바 바이트 코드를 명령어 단위로 읽어 들인다.

JVM의 인터프리터, JIT는 메모리에 적재된 바이트 코드를 명령어 단위로 읽어 해석한다.
각 바이트 코드는 JVM이 이해할 수 있는 명령어로 반횐된다.
해석된 명령어는 CPU에 의해 실행되며, 자바 프로그램 로직이 실제로 수행된다.


1. 인터프리터

바이트 코드를 한 줄씩 읽어 즉시 실행한다.
코드의 해석과 실행이 실시간으로 이루어져 빠르짐만, 성능 면에서는 비효율적일 수 있다.

중복된 바이트 코드는 JIT 컴파일러를 사용한다.


2. JIT Compiler (Just-In-Time)

인터프리터의 단점을 개선하기 위해 등장

반복적으로 호출되는 바이트 코드 블록을 식별하고, 이를 네이티브 머신 코드로 컴파일하여 성능을 향상 시킨다.
JIT 컴파일러는 프로그램 실행 중 필요에 따라 바이트 코드를 네이티브 코드로 변환하여 실행 속도를 높인다.

그 후 캐싱을 통해 동일한 부분이 호출되면 캐싱한 코드를 가져온다.
따라서 한 번 바꾼 Native Code는 인터프리터가 더 이상 컴파일하지 않아도 된다.

즉, 자바 바이트코드에서 Native Code로 바꾸는 과정을 JIT에서 수행한다.


3. Garbage Collections

가비지 컬렉션을 관리하여 힙 메모리에서 더 이상 사용되지 않는 객체를 자동으로 정리한다.

This post is licensed under CC BY 4.0 by the author.