contents
JVM(Java Virtual Machine)이란?
- JVM은 자바 바이트코드를 실행하기 위한 런타임 환경을 제공하는 가상 머신입니다. 즉, 실제 하드웨어나 운영체제와 무관하게 자바 프로그램을 실행할 수 있게 해줍니다.
- JVM 덕분에 자바는 플랫폼 독립적입니다. 즉, 한 번 작성한 자바 프로그램은 어떤 운영체제에서도 JVM만 설치되어 있다면 실행할 수 있습니다. 이를 "Write Once, Run Anywhere(WORA)"라고 부릅니다.
- JVM은 명세로 정의되어 있으며, 여러 구현체(예: Oracle의 HotSpot, Eclipse OpenJ9 등)가 존재합니다.
JVM의 동작 원리
- 자바 소스 코드(.java 파일)는 자바 컴파일러에 의해 바이트코드(.class 파일)로 변환됩니다.
- JVM은 이 .class 파일을 로드하고, 바이트코드를 검증한 후 실행 엔진을 통해 실행합니다.
- JVM은 메모리 관리, 가비지 컬렉션, 보안, 플랫폼 추상화 등 다양한 서비스를 제공합니다.
JVM 아키텍처(구조)
JVM은 여러 주요 하위 시스템과 메모리 영역으로 구성되어 있습니다. 각 구성 요소를 자세히 살펴보겠습니다.
1. 클래스 로더 서브시스템(Class Loader Subsystem)
- 역할: 필요한 시점에 클래스 파일을 JVM으로 동적으로 로드합니다.
- 주요 기능:
- 로딩(Loading): .class 파일을 읽어 바이너리 데이터로 변환
- 링킹(Linking): 클래스 검증, 준비, 해석
- 초기화(Initialization): static 변수에 값 할당, static 블록 실행
- 클래스 로더의 종류:
- 부트스트랩 클래스 로더: Java의 핵심 클래스(rt.jar 등)를 로드
- 확장 클래스 로더: 자바 확장 클래스 로드
- 애플리케이션 클래스 로더: 사용자 애플리케이션 클래스 로드
- 위임 모델: 클래스 로더는 상위 클래스 로더에게 먼저 로딩을 요청하고, 상위가 없을 때만 자신이 로딩
2. 런타임 데이터 영역(JVM 메모리 구조)
| 영역 | 설명 | 공유/스레드별 |
|---|---|---|
| 메서드 영역 | 클래스 메타데이터, static 변수, 메서드 코드, 상수 풀 저장 | 공유 |
| 힙(Heap) | 모든 자바 객체와 배열 저장, 가비지 컬렉션 대상 | 공유 |
| 자바 스택 | 각 스레드마다 존재, 지역 변수, 연산 결과, 메서드 프레임 저장 | 스레드별 |
| PC 레지스터 | 각 스레드마다 존재, 현재 실행 중인 명령어의 주소 저장 | 스레드별 |
| 네이티브 메서드 스택 | 자바가 아닌 네이티브 메서드(C/C++ 등) 호출 시 정보 저장 | 스레드별 |
세부 설명
- 메서드 영역: JVM당 하나, 클래스 수준 데이터, static 필드, 메서드 코드 저장
- 힙: JVM당 하나, 모든 객체 및 배열 저장
- 자바 스택: 스레드마다 하나, 메서드 호출 시마다 스택 프레임 생성
- PC 레지스터: 스레드마다 하나, 현재 실행 중인 명령어 위치 저장
- 네이티브 메서드 스택: JNI 등 네이티브 코드 실행 정보 저장
3. 실행 엔진(Execution Engine)
- 역할: JVM에 로드된 바이트코드를 실제로 실행
- 구성 요소:
- 인터프리터: 바이트코드를 한 줄씩 해석하며 실행
- JIT(Just-In-Time) 컴파일러: 자주 실행되는 바이트코드를 기계어로 변환하여 성능 향상
- 가비지 컬렉터: 더 이상 참조되지 않는 객체를 자동으로 메모리에서 해제
- 동작 방식: 실행 엔진이 바이트코드를 읽어 해석 또는 컴파일하고, 런타임 데이터 영역과 상호작용
4. 네이티브 인터페이스 및 라이브러리
- JNI(Java Native Interface): 자바 코드에서 C/C++ 등 네이티브 코드와 상호작용할 수 있도록 지원
- 네이티브 메서드 라이브러리: 네이티브 메서드 실행에 필요한 외부 라이브러리
JVM의 보안
- JVM은 Security Manager를 통해 보안 정책을 적용하고, 리소스 접근 제어 및 코드 샌드박싱을 제공합니다.
- 바이트코드 검증기는 JVM에 로드된 코드가 메모리 손상이나 권한 침해를 하지 않도록 검증합니다.
JVM의 다양한 구현체
- HotSpot(Oracle/OpenJDK): 가장 널리 쓰이며, 고급 JIT 및 가비지 컬렉션 기능 제공
- Eclipse OpenJ9: 경량, 모듈형, 클라우드 환경에 적합
- 기타: IBM J9, GraalVM 등
JVM과 다른 언어
- JVM은 자바뿐만 아니라 Kotlin, Scala, Groovy, Clojure 등 다양한 언어의 바이트코드도 실행할 수 있습니다.
JVM 아키텍처 다이어그램(텍스트로 표현)
아래는 JVM 아키텍처를 텍스트로 표현한 예시입니다.
+-------------------------------------------------------------+
| Java Virtual Machine (JVM) |
| |
| +-------------------+ +-----------------------------+ |
| | Class Loader | | Runtime Data Areas | |
| | Subsystem | | | |
| +-------------------+ | +----------------------+ | |
| | | Method Area | | |
| | +----------------------+ | |
| | | Heap | | |
| | +----------------------+ | |
| | | Java Stacks | | |
| | +----------------------+ | |
| | | PC Registers | | |
| | +----------------------+ | |
| | | Native Method | | |
| | | Stacks | | |
| | +----------------------+ | |
| +-----------------------------+ |
| +-----------------------------------------------------+ |
| | Execution Engine | |
| | - Interpreter | |
| | - JIT Compiler | |
| | - Garbage Collector | |
| +-----------------------------------------------------+ |
| +-----------------------------------------------------+ |
| | Native Interface (JNI) and Native Libraries | |
| +-----------------------------------------------------+ |
+-------------------------------------------------------------+
JVM의 주요 특징과 장점
- 플랫폼 독립성: 바이트코드는 어떤 OS에서도 JVM만 있으면 실행 가능
- 자동 메모리 관리: 가비지 컬렉션으로 메모리 누수 방지
- 보안: 클래스 검증, 샌드박스, Security Manager 등
- 성능: JIT 컴파일, 최적화된 가비지 컬렉션
- 다양한 언어 지원: 자바 외 다양한 언어 실행 가능
아래는 JVM의 메모리 구조와 **가비지 컬렉션(GC)**에 대한 매우 상세한 설명입니다.
JVM 메모리 구조
JVM의 메모리는 크게 **Heap(힙)**과 Non-Heap(비힙) 영역으로 나뉩니다. 여기서 GC가 직접적으로 관리하는 영역은 주로 힙입니다.
힙(Heap) 메모리
- 힙은 JVM에서 동적으로 생성되는 객체와 배열이 저장되는 공간입니다. JVM이 시작될 때 생성되며, 프로그램 실행 중 크기가 늘거나 줄 수 있습니다.
- 힙은 **Young Generation(영역)**과 **Old Generation(영역)**으로 나뉩니다. (Java 8 이전에는 PermGen, 이후에는 Metaspace가 별도로 존재)
Young Generation (영 영역)
- 모든 새로운 객체는 Young Generation에 할당됩니다.
- Young Generation은 다시 Eden Space와 **두 개의 Survivor Space(S0, S1)**로 분할됩니다.
- Eden Space: 새 객체가 생성되는 공간.
- Survivor Space(S0, S1): GC에서 살아남은 객체가 임시로 이동하는 공간. 두 공간이 번갈아 사용됩니다.
- Minor GC: Young Generation이 가득 차면 발생하는 가비지 컬렉션입니다. 대부분의 객체가 이 단계에서 사라집니다. 살아남은 객체는 Survivor Space로 이동하고, 여러 번 살아남으면 Old Generation으로 승격됩니다.
Old Generation (Tenured, Old 영역)
- Young Generation에서 여러 번 Minor GC를 거쳐 살아남은 장수 객체가 저장됩니다.
- Major GC 또는 Full GC: Old Generation이 가득 차면 수행되는 가비지 컬렉션입니다. Minor GC보다 오래 걸리며, 이 동안 애플리케이션이 일시 중지(Stop-the-World)됩니다.
- Old Generation은 객체가 오래 살아남기 때문에 메모리 단편화가 발생할 수 있으며, 이를 방지하기 위해 메모리 압축(compaction)이 필요합니다.
Metaspace (Java 8 이상)
- 클래스 메타데이터, static 변수, 상수 풀 등 클래스 수준 정보를 저장합니다. PermGen(영구 영역)의 대체로, 힙이 아닌 네이티브 메모리를 사용합니다.
JVM 메모리 구조 예시 (텍스트 다이어그램)
+----------------------------------------------------+
| JVM 메모리 |
| |
| +-------------------+ +---------------------+ |
| | Method Area | | 힙 (Heap) | |
| | (클래스/메타정보) | | | |
| +-------------------+ | +-----------------+ | |
| | | Young Generation| | |
| | | - Eden | | |
| | | - Survivor S0 | | |
| | | - Survivor S1 | | |
| | +-----------------+ | |
| | +-----------------+ | |
| | | Old Generation | | |
| | +-----------------+ | |
| +---------------------+ |
+----------------------------------------------------+
가비지 컬렉션(GC, Garbage Collection)
GC의 기본 원리
- 가비지 컬렉션은 더 이상 참조되지 않는(사용되지 않는) 객체를 자동으로 찾아 메모리를 해제하는 과정입니다.
- JVM은 트레이싱(Tracing) GC를 사용하며, 객체 그래프를 따라가며 살아있는 객체와 죽은 객체를 구분합니다.
- Mark-and-Sweep(마크-스윕): GC는 먼저 살아있는 객체를 "마킹"한 뒤, 마킹되지 않은 객체(더 이상 참조되지 않는 객체)를 "스윕"하여 메모리에서 제거합니다.
세대별(Generational) GC
- 대부분의 객체는 생성 후 곧바로 소멸(단명)한다는 "약한 세대 가설(Weak Generational Hypothesis)"에 기반해, 힙을 Young/Old로 나누고 각 세대별로 다른 알고리즘을 적용합니다.
- Young Generation: 객체가 자주 생성/소멸 → 빠른 Minor GC, Copying 알고리즘 사용.
- Old Generation: 오래 살아남은 객체가 많음 → Major GC, Compaction(압축) 알고리즘 사용.
GC 동작 과정 (예시)
- 객체 생성 시: Eden에 할당.
- Eden이 가득 차면 Minor GC 발생:
- 살아있는 객체는 Survivor S0로 이동, 죽은 객체는 제거.
- 다음 Minor GC:
- Eden + S0의 살아남은 객체는 S1로 이동, 나머지는 제거. S0는 비워짐.
- S0/S1은 번갈아 사용(핑퐁 구조).
- 여러 번 Minor GC에서 살아남으면: 객체는 Old Generation으로 승격(Promotion).
- Old Generation이 가득 차면 Major GC(Full GC) 발생:
- 살아있는 객체만 남기고, 죽은 객체는 제거 및 메모리 압축.
GC의 종류 (JVM 제공)
| GC 종류 | 특징 및 사용처 |
|---|---|
| Serial GC | 단일 스레드, 작은 힙/단일 코어에 적합, 전체 중지 |
| Parallel GC | 멀티스레드, Throughput 중시, 기본 GC |
| CMS GC | 동시 마크-스윕, 짧은 중지, 낮은 레이턴시 |
| G1 GC | 큰 힙/낮은 레이턴시, 영역 분할, 동적 조절 |
| ZGC, Shenandoah | 초저지연, 대용량 힙, 실시간/클라우드 환경 |
추가 정보 및 주의사항
- GC는 Stop-the-World 이벤트: Minor/Full GC 모두 실행 시점에 모든 애플리케이션 스레드를 일시 중지시킵니다. Minor GC는 짧고, Major GC는 길 수 있습니다.
- 메모리 누수: 참조가 남아있는 객체는 GC 대상이 아니므로, 불필요한 참조를 제거하지 않으면 메모리 누수가 발생할 수 있습니다.
- GC 튜닝: JVM 옵션(
-Xms,-Xmx,-XX:NewRatio등)으로 힙 크기와 GC 동작을 조절할 수 있습니다.
요약
- Young Generation: Eden, Survivor S0/S1로 구성, Minor GC로 빠르게 객체 정리.
- Old Generation: 장수 객체 저장, Major(Full) GC로 정리, 메모리 압축 필요.
- GC 알고리즘: Mark-and-Sweep, Copying, Compaction 등 다양한 방식 사용.
- GC 종류: Serial, Parallel, CMS, G1, ZGC, Shenandoah 등.
Garbage-First (G1) 가비지 컬렉터
G1 GC는 Java 9부터 기본 가비지 컬렉터로 채택된 알고리즘으로, 큰 힙을 가진 애플리케이션을 위해 설계되었으며 예측 가능한 일시 정지 시간과 높은 처리량을 목표로 합니다.
주요 특징
- 힙을 **동일 크기의 여러 개 영역(region)**으로 나눕니다(보통 1~32MB 크기, 최대 2048개 영역).
- 세대별(GENERATIONAL) 구조지만, 실제 물리적 구분은 없고 영역 단위로 동적으로 할당됩니다.
- 병렬 및 동시 처리: 많은 작업을 애플리케이션 스레드와 병렬 또는 동시 수행하여 일시 정지 시간을 최소화합니다.
- 일시 정지 시간 목표 설정 가능: 사용자가 목표 일시 정지 시간을 지정하면, G1이 그에 맞게 작업량을 조절합니다.
G1 작동 방식
| 단계 | 설명 |
|---|---|
| Young GC (Minor GC) | Eden과 Survivor 영역의 살아있는 객체를 새로운 Survivor 또는 Old 영역으로 복사합니다. (Stop-the-World 이벤트) |
| 동시 마킹(Concurrent Marking) | 애플리케이션 실행 중에 힙 전체의 살아있는 객체를 마킹합니다. |
| 혼합 GC(Mixed GC) | Young 영역과 가비지가 많은 Old 영역 일부를 함께 수집합니다. |
| 이동(Evacuation) | 가비지가 많은 영역에서 살아있는 객체를 새로운 영역으로 복사해 메모리를 확보하고 단편화를 줄입니다. |
| 압축(Compaction) | 객체를 이동시키면서 메모리를 압축해 단편화를 방지합니다. |
- G1은 각 영역의 살아있는 데이터 양을 추적하여, 가장 많은 가비지를 포함한 영역을 우선적으로 수집합니다("가비지 우선").
- **Remembered Sets(RSet)**를 사용해 영역 간 참조를 관리하여, 전체 힙을 스캔하지 않고도 독립적으로 영역을 수집할 수 있습니다.
- Snapshot-At-The-Beginning (SATB) 알고리즘을 사용해 동시 마킹 시점에 살아있는 객체 스냅샷을 만듭니다.
요약:
G1 GC는 예측 가능한 일시 정지 시간과 큰 힙에 적합하며, 지속적으로 메모리를 압축하여 단편화를 방지합니다.
Concurrent Mark Sweep (CMS) 가비지 컬렉터
CMS GC는 오래된 가비지 컬렉터로, 긴 Stop-the-World 일시 정지를 줄이기 위해 설계된 저지연(로우 레이턴시) 수집기입니다.
주요 특징
- 세대별 구조: Young과 Old 세대를 모두 관리하지만, Old 세대의 동시 수집에 집중합니다.
- 동시 처리: 대부분의 Old Generation 수집 작업을 애플리케이션과 동시에 수행해 일시 정지 시간을 줄입니다.
- 비압축(non-compacting): 기본적으로 메모리 압축을 하지 않아 시간이 지남에 따라 단편화가 발생할 수 있습니다.
CMS 작동 방식
| 단계 | 설명 |
|---|---|
| 초기 마킹(Initial Mark) | 짧은 Stop-the-World 일시 정지. 루트에서 직접 참조하는 객체를 마킹합니다. |
| 동시 마킹(Concurrent Mark) | 애플리케이션이 계속 실행되는 동안 GC 스레드가 객체 그래프를 탐색하며 살아있는 객체를 찾습니다. |
| 동시 사전 정리(Concurrent Pre-clean) | 마킹 중 변경된 객체를 추가로 마킹합니다. |
| 재마킹(Remark) | 또 다른 Stop-the-World 일시 정지. 동시 마킹 중 놓친 객체를 마킹합니다. |
| 동시 스윕(Concurrent Sweep) | 애플리케이션 실행 중에 도달 불가능한 객체를 제거하고 메모리를 해제합니다. |
| 리셋(Reset) | 다음 GC 주기를 준비합니다. |
- Young Generation은 보통 병렬 수집기(ParNew)로 수집합니다.
- 부동 가비지(Floating Garbage): 애플리케이션이 동시 마킹 중에도 객체를 수정하기 때문에, 일부 가비지는 다음 GC 주기까지 수집되지 않을 수 있습니다.
- 동시 모드 실패(Concurrent Mode Failure): CMS가 Old Generation을 다 수집하지 못하면, 긴 Stop-the-World Full GC가 발생합니다.
요약:
CMS는 대부분 작업을 동시 처리해 일시 정지 시간을 줄이지만, 단편화 문제와 CPU 자원 소모가 크며 최신 Java 버전에서는 G1 GC로 대체되고 있습니다.
Z Garbage Collector (ZGC)
ZGC는 Java 11에 실험적으로 도입되어 이후 안정화된 초저지연(ultra-low latency) 가비지 컬렉터입니다. 매우 큰 힙(수십 GB~TB 규모)과 엄격한 일시 정지 시간 요구에 적합합니다.
주요 특징
- 영역 기반(region-based): 힙을 매우 작은 영역(Z-Page)으로 나누어 동적으로 할당 및 해제합니다.
- 대부분 동시 처리: 거의 모든 GC 작업을 애플리케이션과 동시에 수행하며, 일시 정지 시간은 보통 1ms 이하입니다.
- **초기에는 비세대(non-generational)**였으나, Java 21부터는 세대별 ZGC가 도입되어 효율성 향상.
- 항상 압축(compacting): 메모리 단편화를 없애기 위해 객체를 이동하며 압축합니다.
- 컬러드 포인터(Colored Pointers)와 로드 배리어(Load Barriers): 객체 이동과 참조 업데이트를 안전하게 처리하는 첨단 기술 사용.
ZGC 작동 방식
| 단계 | 설명 |
|---|---|
| 일시 정지 마킹 시작(Pause Mark Start) | 매우 짧은 일시 정지로 마킹 시작 신호를 보냅니다. |
| 동시 마킹(Concurrent Mark) | 애플리케이션 실행 중에 살아있는 객체를 마킹합니다. |
| 일시 정지 마킹 종료(Pause Mark End) | 짧은 일시 정지로 마킹을 마무리합니다. |
| 동시 이동 준비(Concurrent Prepare for Relocation) | 객체 이동 준비를 동시 처리합니다. |
| 일시 정지 이동 시작(Pause Relocate Start) | 이동 시작을 알리는 짧은 일시 정지입니다. |
| 동시 이동(Concurrent Relocate) | 애플리케이션이 계속 실행되는 동안 객체를 이동시키고, 로드 배리어와 컬러드 포인터로 참조를 안전하게 업데이트합니다. |
- 로드 배리어(Load Barriers): 애플리케이션 코드에 삽입되어 객체 참조 접근 시 참조가 최신 상태인지 확인하고 필요 시 업데이트합니다.
- 컬러드 포인터(Colored Pointers): GC가 객체 상태와 위치를 추적하는 데 사용합니다.
- 세대별 ZGC(Java 21 이상): Young/Old 세대 구분을 추가해 Young 영역을 더 자주 수집하고, 세대 간 참조를 효율적으로 관리합니다.
요약:
ZGC는 매우 큰 힙과 초저지연을 요구하는 애플리케이션에 적합하며, 거의 모든 GC 작업을 동시 처리하여 일시 정지 시간을 극도로 줄입니다.
비교표
| GC 종류 | 힙 구조 | 세대별 여부 | 압축 여부 | 동시 처리 정도 | 일시 정지 시간 | 적합한 용도 |
|---|---|---|---|---|---|---|
| G1 | 영역(region) 기반 | 예 | 예 | 부분적 | 낮고 예측 가능 | 큰 힙, 예측 가능한 지연 시간 필요 |
| CMS | 연속 공간 | 예 | 아니오 | 부분적 | 낮음 (Old Gen) | 저지연, 레거시 시스템 |
| ZGC | 영역(Z-Page) 기반 | 초기엔 아니오 (Java 21부터 예) | 예 | 거의 전부 | 초저지연 (<1ms) | 매우 큰 힙, 초저지연 요구 |
references