Skip to content

Java Virtual Machine

자바 가상 머신(JVM)은 자바 애플리케이션을 실행하기 위한 런타임 엔진이다.

Java Application
JVM(Windows/Mac/Linux)
OS(Windows/Mac/Linux)
Computer(Hardware)

자바 코드는 이 JVM 위에서 동작하며, 전체 실행 환경은 ‘하드웨어 - 운영체제(OS) - JVM - 자바 애플리케이션’ 순서의 계층 구조를 가진다.

JVM의 존재는 자바 언어의 다음과 같은 특징을 결정한다.

  • 플랫폼 독립성
    • 자바 소스 코드는 특정 OS를 대상으로 컴파일되지 않고, JVM을 목적지로 하는 ‘자바 바이트 코드(*.class)‘로 변환
    • 이 바이트 코드는 해당 OS에 맞는 JVM이 설치되어 있기만 하면, 어떤 플랫폼(Windows, Mac, Linux 등)에서든 수정 없이 동일하게 실행 가능
  • JVM 실행 필수
    • 플랫폼 독립성을 얻는 대신, 자바 코드를 실행하기 위해서는 반드시 대상 플랫폼에 JVM 설치 필요
  • 성능 최적화
    • 과거에는 OS 위에서 가상 머신을 한 단계 더 거치기 때문에 네이티브 코드(C/C++) 대비 실행 속도가 느리다는 단점 존재
    • 현재는 JIT(Just-In-Time) 컴파일러와 같은 최적화 기술의 발전으로, 런타임 중에 자주 사용되는 코드를 네이티브 기계어로 변환하여 실행 속도 향상
flowchart TD
SRC["JAVA Source<br/>(.java)"] -->|" JAVA Compiler (javac) "| BC["JAVA Byte Code<br/>(.class)"]
BC --> CL
subgraph JVM["JVM (JAVA Virtual Machine)"]
CL[Class Loader]
RDA[Runtime Data Area]
EE[Execution Engine]
CL --> RDA
CL --> EE
RDA <--> EE
end
  • Java Compiler: Java Source Code를 Java Byte Code로 변환
  • Class Loader: 바이트 코드 로딩 / 검증 / 링킹 등 수행
  • Runtime Data Area: 앱 실행을 위해 사용되는 JVM 메모리 영역
  • Execution Engine: 메모리 영역에 있는 데이터를 가져와 해당하는 작업 수행
  1. 작성된 Java Source를 Java Compiler를 통해 Java Byte Code로 변환
  2. 컴파일 된 Byte Code를 JVM의 Class Loader에 전달
  3. Class Loader는 Dynamic Loading을 통해 필요한 클래스들을 로딩 및 링크하여 Runtime Data Area(JVM Memory)로 전달
  4. Execution Engine이 올라온 Byte Code들을 명령어 단위로 하나씩 가져와서 실행

클래스 로더는 컴파일된 자바 클래스 파일(*.class)을 메모리로 로드하고, Runtime Data Area에 배치하는 역할을 한다.

  • 로딩 단계

    1. 로딩(Loading): 클래스 파일을 찾아 바이트 코드를 메모리에 로드
      • 한 번에 모든 클래스를 로드하는 것이 아니라, 필요할 때 동적으로 로드
      • static 멤버들 또한 전부 메모리에 올라가는 것이 아니라, 클래스 내의 static 멤버를 호출하게 되면 클래스가 동적으로 메모리에 로드
    2. 링크(Linking): 읽어온 코드를 실행 가능하도록 준비
      • 검증(Verify): 바이트 코드가 자바 언어 명세 및 JVM 명세를 준수하는지 확인(보안)
      • 준비(Prepare): 클래스의 정적(static) 변수들을 위한 메모리를 할당하고 기본값(0, false, null 등)으로 초기화
      • 분석(Resolve): 코드 내의 기호 참조(Symbolic Reference)를 실제 메모리 주소(Direct Reference)로 변경
    3. 초기화(Initialization): ‘준비’ 단계에서 기본값으로 초기화했던 정적 변수들을 실제 코드에 명시된 값(static 블록 포함)으로 초기화
  • 클래스 로더 위임 모델(Delegation Model): 클래스 로더는 계층 구조를 가지며, 클래스 로드 요청 시 하위 로더가 상위 로더에게 책임을 위임하는 방식으로 동작

    • 부트스트랩(Bootstrap) 로더: 최상위 로더. JVM 핵심 라이브러리(JAVA_HOME/lib의 rt.jar 등)를 로드
    • 확장(Extension) 로더: lib/ext 폴더의 클래스 로드
    • 애플리케이션(Application/System) 로더: 사용자가 지정한 클래스패스(Classpath)의 클래스 로드
    • 이 구조는 이미 로드된 클래스의 중복 로드를 방지하고, 핵심 라이브러리의 보안을 유지하는 역할

클래스 로더가 메모리에 적재한 바이트 코드를 실제 기계어로 변환하고 실행하는 역할을 한다.

  • 인터프리터 (Interpreter)
    • 바이트 코드를 한 줄씩 읽어서 해석하고(interpret) 바로 실행
    • 초기 실행 속도는 빠르지만, 동일한 코드가 반복 호출될 때도 매번 해석해야 하므로 비효율적일 수 있음
  • JIT 컴파일러 (Just-In-Time Compiler)
    • 인터프리터의 단점을 보완하기 위해 도입
    • 애플리케이션 실행 중에(Just-In-Time) 반복적으로 실행되는 ‘핫스팟(hotspot)’ 코드 감지
    • ‘핫스팟’ 코드를 네이티브 기계어로 컴파일하여 캐시에 저장
    • 이후 해당 코드가 호출되면, 인터프리트 방식이 아닌 캐시된 네이티브 코드를 직접 실행하여 성능 향상
  • 가비지 컬렉터 (Garbage Collector, GC)
    • 실행 엔진의 일부로 동작하며, 힙(Heap) 메모리 영역에서 더 이상 참조되지 않는 객체(가비지)를 찾아 제거하고 메모리 회수
  • JVM(Java Virtual Machine): 자바 바이트 코드를 실행시키기 위한 가상 머신
  • JRE(Java Runtime Environment): 자바 애플리케이션을 실행하기 위한 도구(필요한 라이브러리 및 필수 파일)가 포함된 실행 환경(JRE = JVM + Standard Libraries)
  • JDK(Java Development Kit): 자바로 개발하기 위한 필요 요소(javac 등)를 포함한 개발 키트(JDK = JRE + Development Tools)

위와 같은 구조로 인해 자바 애플리케이션을 배포하여 실행만 할 서버에는 JRE만 설치하고, 개발자의 로컬 장비에는 JDK를 설치한다.

JVM은 OS로부터 실행에 필요한 메모리를 할당받으며, 이 영역을 Runtime Data Area라고 부른다.

영역용도생명 주기스레드 공유 여부
Method클래스 정보, 클래스(static) 변수, 상수, 메소드 코드JVM 시작 ~ 종료O
Heap객체 인스턴스, 인스턴스 변수Gabage Collection에 의해 관리O
Stack스레드 별로 런타임에 호출 된 메서드, 지역 변수, 매개 변수, 리턴 값메서드 종료 시X

스레드 공유 영역 (모든 스레드가 공유)

Section titled “스레드 공유 영역 (모든 스레드가 공유)”
  • 힙 (Heap Area)
    • new 키워드로 생성된 객체 인스턴스와 배열이 저장되는 공간
    • 가비지 컬렉션(GC)의 주된 대상
    • 성능 최적화를 위해 내부적으로 Young Generation(Eden, Survivor 0/1)과 Old Generation 영역으로 나뉘어 관리
    • 이 영역의 메모리가 부족하면 OutOfMemoryError가 발생
  • 메소드 영역 (Method Area)
    • 클래스의 메타데이터(구조, 필드, 메소드 정보), 정적(static) 변수, 상수 풀(Runtime Constant Pool), 메소드 코드 등 저장
    • 자바 8 이전에는 이 영역을 힙의 일부로 취급
    • 자바 8부터는 힙이 아닌 네이티브 메모리 영역(OS가 직접 관리)을 사용하도록 변경(OutOfMemoryError 문제가 크게 개선)

스레드 독립 영역 (스레드별 개별 생성)

Section titled “스레드 독립 영역 (스레드별 개별 생성)”

각 스레드는 생성될 때마다 이 영역들을 개별적으로 할당받는다.

  • 스택 (Stack Area)
    • 메소드 호출 정보를 저장하는 영역
    • 메소드가 호출될 때마다 해당 메소드의 정보(지역 변수, 매개 변수, 리턴 주소 등)를 담은 스택 프레임(Stack Frame)이 생성되어 스택에 쌓이고, 메소드 실행이 완료되면 제거
    • 스택 영역의 한계를 초과하면 StackOverflowError가 발생
  • PC 레지스터 (PC Register)
    • 현재 스레드가 실행 중인 JVM 명령어의 주소 저장
    • 스레드가 컨텍스트 스위칭을 할 때, 다음에 실행할 명령어를 기억하기 위해 사용
  • 네이티브 메소드 스택 (Native Method Stack)
    • 자바 코드(바이트 코드)가 아닌 C/C++ 등 네이티브 코드로 작성된 메소드를 호출할 때 사용되는 별도의 스택 영역
    • 각 자바 스레드는 Java Stack과 Native Method Stack을 함께 보유하며, 실행 중인 코드의 종류에 따라 해당 스택에 프레임이 쌓임

자바는 JVM 위에서 실행되지만, OS 저수준 기능에 접근하거나 기존에 작성된 C/C++ 라이브러리를 활용할 때 가능하게 해주는 표준 인터페이스인 JNI가 존재한다.

JNI는 자바 코드에서 C/C++ 같은 네이티브 언어로 작성된 함수를 호출할 수 있도록 해주는 표준 인터페이스이다.

  • 사용 배경
    • 자바 언어만으로는 OS 커널 기능(파일 I/O, 네트워크 소켓, 시스템 시간 등 저수준 연산) 직접 접근 불가
    • 성능이 중요하거나 이미 존재하는 C/C++ 라이브러리(암호화, 이미지 처리, 압축 등) 재사용 필요
    • JDK 내부에서도 OS와 상호작용하는 대부분의 코드가 JNI 호출 기반
  • 호출 방식
    • 자바 측에서는 메서드에 native 키워드를 붙여 시그니처만 선언하고, 실제 구현은 별도의 공유 라이브러리 파일(.dll / .so / .dylib)로 제공
    • 애플리케이션이 해당 메서드를 호출하면 JVM이 공유 라이브러리를 찾아 실제 C/C++ 함수로 실행 흐름 전달
  • 표준 라이브러리 내 대표적 사용 예
    • FileInputStream.read() 등 파일 시스템 호출
    • Object.hashCode(), System.currentTimeMillis() 등 기본 메서드
    • java.util.zip 압축·해제 시 내부적으로 zlib 네이티브 라이브러리 호출

자바 메서드를 호출하면 Java Stack에 자바 프레임이 쌓이는 것과 마찬가지로, JNI를 통해 네이티브 메서드를 호출하면 Native Method Stack에 네이티브 프레임이 쌓인다.

구분자바 프레임 (Java Frame)네이티브 프레임 (Native Frame)
생성 시점자바 메서드 호출 시JNI 네이티브 메서드 호출 시
저장 영역Java StackNative Method Stack
관리 주체JVMOS
내부 데이터자바 지역 변수, 매개 변수, 바이트 코드 포인터C 지역 변수, 포인터, 네이티브 명령어 주소
힙 복사 가능성JVM이 포맷을 알고 있어 힙으로 안전하게 복사 가능OS 스택 주소·레지스터에 종속되어 복사 불가
  • 자바 프레임: JVM이 직접 설계한 포맷이므로 내용 전체를 파악하고 조작 가능
  • 네이티브 프레임: OS가 관리하는 일반 C 함수 스택이므로 JVM 입장에서는 내부 구조를 알 수 없는 불투명 메모리 블록

이 차이는 평소에는 드러나지 않지만, 스택을 힙으로 옮겨야 하는 특수한 상황에서 제약으로 작용한다.

가상 스레드는 I/O 블로킹 시 현재 스택을 힙으로 옮겨 캐리어 스레드(실제 OS 스레드)에서 내려오는 방식으로 동작하는데, 이때 네이티브 프레임이 문제가 된다.

  • 자바 프레임만으로 이루어진 스택: 힙으로 안전하게 복사되어 가상 스레드가 캐리어에서 내려옴
  • 네이티브 프레임이 포함된 스택: OS 의존 데이터 때문에 복사 불가로 캐리어 스레드에 고정(Pinning)
  • 결과: 해당 가상 스레드가 I/O 대기 중에도 캐리어 스레드를 점유하여 다른 가상 스레드의 실행 기회 상실

Last updated:

Java