Java Virtual Machine
자바 가상 머신(JVM)은 자바 애플리케이션을 실행하기 위한 런타임 엔진이다.
| Java Application |
|---|
| JVM(Windows/Mac/Linux) |
| OS(Windows/Mac/Linux) |
| Computer(Hardware) |
자바 코드는 이 JVM 위에서 동작하며, 전체 실행 환경은 ‘하드웨어 - 운영체제(OS) - JVM - 자바 애플리케이션’ 순서의 계층 구조를 가진다.
JVM의 주요 특징
Section titled “JVM의 주요 특징”JVM의 존재는 자바 언어의 다음과 같은 특징을 결정한다.
- 플랫폼 독립성
- 자바 소스 코드는 특정 OS를 대상으로 컴파일되지 않고, JVM을 목적지로 하는 ‘자바 바이트 코드(*.class)‘로 변환
- 이 바이트 코드는 해당 OS에 맞는 JVM이 설치되어 있기만 하면, 어떤 플랫폼(Windows, Mac, Linux 등)에서든 수정 없이 동일하게 실행 가능
- JVM 실행 필수
- 플랫폼 독립성을 얻는 대신, 자바 코드를 실행하기 위해서는 반드시 대상 플랫폼에 JVM 설치 필요
- 성능 최적화
- 과거에는 OS 위에서 가상 머신을 한 단계 더 거치기 때문에 네이티브 코드(C/C++) 대비 실행 속도가 느리다는 단점 존재
- 현재는 JIT(Just-In-Time) 컴파일러와 같은 최적화 기술의 발전으로, 런타임 중에 자주 사용되는 코드를 네이티브 기계어로 변환하여 실행 속도 향상
Java 실행 과정
Section titled “Java 실행 과정”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: 메모리 영역에 있는 데이터를 가져와 해당하는 작업 수행
- 작성된 Java Source를 Java Compiler를 통해 Java Byte Code로 변환
- 컴파일 된 Byte Code를 JVM의 Class Loader에 전달
- Class Loader는 Dynamic Loading을 통해 필요한 클래스들을 로딩 및 링크하여 Runtime Data Area(JVM Memory)로 전달
- Execution Engine이 올라온 Byte Code들을 명령어 단위로 하나씩 가져와서 실행
Class Loader
Section titled “Class Loader”클래스 로더는 컴파일된 자바 클래스 파일(*.class)을 메모리로 로드하고, Runtime Data Area에 배치하는 역할을 한다.
-
로딩 단계
- 로딩(Loading): 클래스 파일을 찾아 바이트 코드를 메모리에 로드
- 한 번에 모든 클래스를 로드하는 것이 아니라, 필요할 때 동적으로 로드
- static 멤버들 또한 전부 메모리에 올라가는 것이 아니라, 클래스 내의 static 멤버를 호출하게 되면 클래스가 동적으로 메모리에 로드
- 링크(Linking): 읽어온 코드를 실행 가능하도록 준비
- 검증(Verify): 바이트 코드가 자바 언어 명세 및 JVM 명세를 준수하는지 확인(보안)
- 준비(Prepare): 클래스의 정적(static) 변수들을 위한 메모리를 할당하고 기본값(0, false, null 등)으로 초기화
- 분석(Resolve): 코드 내의 기호 참조(Symbolic Reference)를 실제 메모리 주소(Direct Reference)로 변경
- 초기화(Initialization): ‘준비’ 단계에서 기본값으로 초기화했던 정적 변수들을 실제 코드에 명시된 값(static 블록 포함)으로 초기화
- 로딩(Loading): 클래스 파일을 찾아 바이트 코드를 메모리에 로드
-
클래스 로더 위임 모델(Delegation Model): 클래스 로더는 계층 구조를 가지며, 클래스 로드 요청 시 하위 로더가 상위 로더에게 책임을 위임하는 방식으로 동작
- 부트스트랩(Bootstrap) 로더: 최상위 로더. JVM 핵심 라이브러리(JAVA_HOME/lib의 rt.jar 등)를 로드
- 확장(Extension) 로더:
lib/ext폴더의 클래스 로드 - 애플리케이션(Application/System) 로더: 사용자가 지정한 클래스패스(Classpath)의 클래스 로드
- 이 구조는 이미 로드된 클래스의 중복 로드를 방지하고, 핵심 라이브러리의 보안을 유지하는 역할
Execution Engine
Section titled “Execution Engine”클래스 로더가 메모리에 적재한 바이트 코드를 실제 기계어로 변환하고 실행하는 역할을 한다.
- 인터프리터 (Interpreter)
- 바이트 코드를 한 줄씩 읽어서 해석하고(interpret) 바로 실행
- 초기 실행 속도는 빠르지만, 동일한 코드가 반복 호출될 때도 매번 해석해야 하므로 비효율적일 수 있음
- JIT 컴파일러 (Just-In-Time Compiler)
- 인터프리터의 단점을 보완하기 위해 도입
- 애플리케이션 실행 중에(Just-In-Time) 반복적으로 실행되는 ‘핫스팟(hotspot)’ 코드 감지
- ‘핫스팟’ 코드를 네이티브 기계어로 컴파일하여 캐시에 저장
- 이후 해당 코드가 호출되면, 인터프리트 방식이 아닌 캐시된 네이티브 코드를 직접 실행하여 성능 향상
- 가비지 컬렉터 (Garbage Collector, GC)
- 실행 엔진의 일부로 동작하며, 힙(Heap) 메모리 영역에서 더 이상 참조되지 않는 객체(가비지)를 찾아 제거하고 메모리 회수
JDK & JRE & JVM
Section titled “JDK & JRE & JVM”- JVM(Java Virtual Machine): 자바 바이트 코드를 실행시키기 위한 가상 머신
- JRE(Java Runtime Environment): 자바 애플리케이션을 실행하기 위한 도구(필요한 라이브러리 및 필수 파일)가 포함된 실행 환경(JRE = JVM + Standard Libraries)
- JDK(Java Development Kit): 자바로 개발하기 위한 필요 요소(javac 등)를 포함한 개발 키트(JDK = JRE + Development Tools)
위와 같은 구조로 인해 자바 애플리케이션을 배포하여 실행만 할 서버에는 JRE만 설치하고, 개발자의 로컬 장비에는 JDK를 설치한다.
JVM 메모리 구조
Section titled “JVM 메모리 구조”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을 함께 보유하며, 실행 중인 코드의 종류에 따라 해당 스택에 프레임이 쌓임
JNI와 네이티브 프레임
Section titled “JNI와 네이티브 프레임”자바는 JVM 위에서 실행되지만, OS 저수준 기능에 접근하거나 기존에 작성된 C/C++ 라이브러리를 활용할 때 가능하게 해주는 표준 인터페이스인 JNI가 존재한다.
JNI(Java Native Interface)
Section titled “JNI(Java Native Interface)”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 네이티브 라이브러리 호출
네이티브 프레임 (Native Frame)
Section titled “네이티브 프레임 (Native Frame)”자바 메서드를 호출하면 Java Stack에 자바 프레임이 쌓이는 것과 마찬가지로, JNI를 통해 네이티브 메서드를 호출하면 Native Method Stack에 네이티브 프레임이 쌓인다.
| 구분 | 자바 프레임 (Java Frame) | 네이티브 프레임 (Native Frame) |
|---|---|---|
| 생성 시점 | 자바 메서드 호출 시 | JNI 네이티브 메서드 호출 시 |
| 저장 영역 | Java Stack | Native Method Stack |
| 관리 주체 | JVM | OS |
| 내부 데이터 | 자바 지역 변수, 매개 변수, 바이트 코드 포인터 | C 지역 변수, 포인터, 네이티브 명령어 주소 |
| 힙 복사 가능성 | JVM이 포맷을 알고 있어 힙으로 안전하게 복사 가능 | OS 스택 주소·레지스터에 종속되어 복사 불가 |
- 자바 프레임: JVM이 직접 설계한 포맷이므로 내용 전체를 파악하고 조작 가능
- 네이티브 프레임: OS가 관리하는 일반 C 함수 스택이므로 JVM 입장에서는 내부 구조를 알 수 없는 불투명 메모리 블록
이 차이는 평소에는 드러나지 않지만, 스택을 힙으로 옮겨야 하는 특수한 상황에서 제약으로 작용한다.
가상 스레드 피닝과의 연관성
Section titled “가상 스레드 피닝과의 연관성”가상 스레드는 I/O 블로킹 시 현재 스택을 힙으로 옮겨 캐리어 스레드(실제 OS 스레드)에서 내려오는 방식으로 동작하는데, 이때 네이티브 프레임이 문제가 된다.
- 자바 프레임만으로 이루어진 스택: 힙으로 안전하게 복사되어 가상 스레드가 캐리어에서 내려옴
- 네이티브 프레임이 포함된 스택: OS 의존 데이터 때문에 복사 불가로 캐리어 스레드에 고정(Pinning)
- 결과: 해당 가상 스레드가 I/O 대기 중에도 캐리어 스레드를 점유하여 다른 가상 스레드의 실행 기회 상실