JVM(자바 가상 머신)은 Java 프로그램을 실행하기 위한 가상 머신이다
즉, 자바 코드를 실행할 수 있는 환경을 제공하는 소프트웨어이다 !
JVM은 크게 클래스 로더, 런타임 데이터 영역, 실행 엔진, 네이티브 인터페이스로 구성
클래스 로더 (ClassLoader)
.class 파일(바이트코드)을 JVM 메모리에 로딩하는 역할
실행 시 필요한 클래스들을 로드하고, 링크하고, 초기화함
3가지 주요 로더
lib/ext에 있는 확장 클래스 로드)동작 과정
static) 초기화런타임 데이터 영역 (Runtime Data Area)
JVM이 프로그램 실행 중 데이터를 저장하는 공간으로, 여러 영역으로 나뉨
실행 엔진 (Execution Engine)
JVM이 바이트코드를 실제 기계어로 변환하고 실행하는 부분
네이티브 인터페이스 (Native Interface)
JVM이 운영체제(OS)나 네이티브 코드(C, C++)와 상호작용할 수 있도록 도와줌
JNI(Java Native Interface)를 통해 네이티브 라이브러리를 호출할 수 있음
클래스 로더(ClassLoader)는 JVM이
.class파일(바이트코드)을
메모리에 로드하는 역할을 한다. 즉, Java 프로그램 실행 시 필요한 클래스를 찾아서
JVM의 메모리에 올리는 과정을 담당한다 !
👀 클래스 로더의 역할
1. 클래스를 동적으로 로드(Loading)
.class 파일로 변환됨 → 실행 시 JVM이 필요할 때 메모리에 적재2. 클래스를 링크(Linking)
3. 클래스를 초기화(Initialization)
static 변수 초기화 및 static {} 블록 실행👀 클래스 로더의 종류
JVM에는 기본적으로 3가지 클래스 로더가 존재한다.
클래스를 부모 → 자식 순서로 로드하는
부트스트랩 방식 (Delegation Model, 위임 모델)을 사용한다 !
1. 부트스트랩 클래스 로더 (Bootstrap ClassLoader)
2. 확장 클래스 로더 (Extension ClassLoader)
3. 애플리케이션 클래스 로더 (Application ClassLoader)
👀 클래스 로딩 과정 (3단계)
1. 로딩 (Loading)
.class 파일을 찾아서 JVM의 메소드 영역(Method Area)에 저장static 변수와 메서드 정보를 메모리에 올림2. 링크 (Linking)
.class 파일이 올바른지 체크static 변수 메모리 할당3. 초기화 (Initialization)
static 블록 실행 및 static 변수 초기화👀 클래스 로더 동작 방식 (Delegation Model)
클래스를 로드할 때, 부모 → 자식 순서로 요청을 위임하는 방식이다.
/* 클래스 로딩 요청 흐름 */
사용자 클래스 요청 → 애플리케이션 클래스 로더
│
├── 부모(확장 클래스 로더)에게 요청
│ ├── 부모(부트스트랩 클래스 로더)에게 요청
│ │ ├── 요청한 클래스 찾음 → 로드
│ │ ├── 찾을 수 없음 → 자식에게 요청
│ ├── 요청한 클래스 찾음 → 로드
│ ├── 찾을 수 없음 → 자식에게 요청
├── 요청한 클래스 찾음 → 로드
├── 찾을 수 없음 → ClassNotFoundException 발생
부모가 먼저 클래스를 찾고, 없으면 자식이 찾음!
안전한 클래스 로딩을 위해 JVM이 사용하는 방식
JVM(Java Virtual Machine) 메모리는 Java 프로그램 실행 중
데이터를 저장하는 공간이다. JVM 메모리는 여러 영역으로 나뉘어 관리되며,
효율적인 실행을 위해 역할별로 구분돼 있다.
JVM 메모리는 5가지 주요 영역으로 나뉜다.
메서드(Method) 영역 → 클래스 정보 & static 변수 저장
힙(Heap) 영역 → 객체(instance) 저장 (GC의 대상)
스택(Stack) 영역 → 메서드 실행 시 생성되는 지역 변수 저장
PC 레지스터(PC Register) → 현재 실행 중인 명령어의 주소 저장
네이티브 메서드 스택(Native Stack) → Java가 아닌 네이티브 코드(C/C++) 실행 시 사용
1. 메서드(Method) 영역
메서드 영역은 JVM이 클래스 정보를 저장하는 공간이다.
final 상수 값 저장2. 힙(Heap) 영역
힙 영역은 JVM이 객체(instance)를 저장하는 공간이다.
자바의 GC(Garbage Collector)가 관리하는 영역으로, 사용되지 않는 객체를
자동으로 제거한다 !
3. 스택(Stack) 영역
스택 영역은 메서드가 실행될 때 생성되는 지역 변수와 호출 정보를 저장하는 공간이다.
각 메서드 호출마다 스택 프레임(Stack Frame)이 생성되고, 메서드 실행이 끝나면 제거된다 !
4. PC 레지스터 (PC Register)
PC 레지스터(Program Counter Register)는 현재 실행 중인 명령어의
메모리 주소를 저장하는 공간이다.
각 스레드마다 하나의 PC 레지스터를 가짐 (멀티스레드 환경에서 중요!)
✔ 메서드 실행 흐름을 관리하는 역할!
5. 네이티브 메서드 스택 (Native Method Stack)
네이티브 메서드 스택은 Java가 아닌 네이티브 코드(C, C++ 등)를 실행할 때 사용하는 공간이다.
Java 프로그램이 네이티브 메서드를 호출하면, 해당 메서드의 실행 정보를 관리한다 !
GC(Garbage Collection, 가비지 컬렉션)은 JVM이 더 이상 사용되지 않는 객체를
자동으로 제거하여 메모리를 관리하는 기능이다. 즉, 힙(Heap) 영역에서 필요 없는
객체를 찾아 삭제해 메모리 누수를 방지하는 역할을 한다 !
👀 GC 동작 과정 (Mark & Sweep 알고리즘)
JVM의 GC는 객체를 정리하는 과정을 다음 2단계로 진행한다.
1. 마킹(Marking)
2. 삭제(Sweeping)
GC 대상이란 ?
‣ 더 이상 참조되지 않는 객체
‣ 스택에서 사용되지 않는 객체
‣ 클래스 로더가 제거된 후 남아 있는 클래스 데이터
😄 GC의 장점
1. 메모리 자동 관리 (수동 해제 불필요)
2. 메모리 누수(Leak) 방지
delete를 빼먹으면 메모리 누수가 발생하지만,3. Dangling Pointer(잘못된 메모리 참조) 문제 방지
4. JVM이 OS에 독립적으로 메모리 관리
→ 한 번 개발하면 Windows, Linux, Mac에서 동일하게 실행 가능!
5. GC 최적화를 통한 성능 개선 (최신 GC 알고리즘 지원)
😔 GC의 단점
1. GC 실행 시 성능 저하
→ 객체가 너무 많이 생성되면 GC가 자주 실행되고,
STW로 인해 애플리케이션이 멈추는 시간이 증가
2. 높은 CPU 사용량 (GC Overhead)
3. 메모리 단편화 문제
4. GC 실행 시 애플리케이션 제어 어려움
System.gc();를 호출하여 GC 실행 요청은 가능하지만, JVM이 반드시 실행하는 것은 아님1. GC 실행 시간 & Stop-the-World(STW) 발생 시간 확인
2. 메모리 누수(Leak) 및 OutOfMemoryError(OOM) 감지
3. GC 알고리즘 선택 및 최적화
OutOfMemoryError는 JVM이 더 이상 사용할 수 있는 메모리가 없을 때 발생하는
오류이다. OOM이 발생하면 애플리케이션이 비정상적으로 종료될 수 있기 때문에
빠른 대응이 필요하다 !
⚠️ OOM 발생 유형 및 해결 방법
1. Java Heap Space 부족 (가장 흔한 OOM)
원인
해결 방법
Xmx 옵션을 설정하여 힙 메모리 크기를 늘릴 수 있음WeakReference 또는 SoftReference 활용하여 GC가 객체를 더 빨리 정리하도록 유도null로 해제하여 GC가 제거할 수 있도록 함2. GC Overhead Limit Exceeded (GC가 과부하)
원인
해결 방법
-Xmx 옵션 설정)StringBuilder 사용3. Direct ByteBuffer OOM (네이티브 메모리 부족)
원인
ByteBuffer.allocateDirect()를 사용할 때 네이티브 메모리를 너무 많이 사용하면 발생해결 방법:
-XX:MaxDirectMemorySize)ByteBuffer 사용 후 clear() 호출1. JVM 옵션을 활용한 GC 로그 분석
/* GC 로그 예제 */
[GC (Allocation Failure) 512K->256K(1024K), 0.0056780 secs]
[GC (Allocation Failure) 768K->384K(1024K), 0.0034200 secs]
→ GC가 너무 자주 실행되고 힙(Heap) 메모리가 줄어들지 않는다면
메모리 누수를 의심해야 함 !
2. JVisualVM을 활용한 Heap Dump 분석
JVisualVM 실행 → 실행 중인 Java 애플리케이션 선택
→ Heap Dump 생성 후 분석 (Profiler → Heap Dump)
→ 누수가 의심되는 객체 확인 (Instances 탭에서 계속 유지되는 객체 찾기)
3. Heap Dump 분석 (Eclipse MAT 사용)
4. WeakReference 활용하여 누수 감지
WeakReference로 감싸면→ GC 후 null이면 정상적으로 해제됨, 그렇지 않으면 누수 가능성 있음!
🪢 GC에서 사용하는 대표적인 알고리즘
| 알고리즘 | 설명 | 장점 | 단점 |
|---|---|---|---|
| Mark-Sweep (마크 & 스윕) | 살아있는 객체를 마킹 후 삭제 | 단순한 구현, 효율적 | 메모리 단편화 발생 가능 |
| Mark-Compact (마크 & 컴팩트) | 객체를 마킹 후 삭제 + 압축(Compaction) | 메모리 단편화 방지 | 추가적인 연산 필요 |
| Copying (복사 GC) | 객체를 두 개의 영역으로 나누고 한쪽으로 복사 | 빠른 할당, 단편화 없음 | 메모리 사용량 증가 |
| Generational GC (세대별 GC) | 객체를 Young, Old 세대로 나누어 관리 | GC 성능 최적화 | GC 전략이 복잡함 |
| Reference Counting (참조 카운팅) | 객체 참조 횟수를 카운팅하여 관리 | 즉시 수거 가능 | 순환 참조 문제 발생 가능 |
→ Java는 Mark-Sweep, Mark-Compact, Copying 방식을 조합한
"세대별 GC(Generational GC)"를 사용함 !
🪢 Java의 GC 알고리즘 종류
| GC 유형 | 특징 | 장점 | 단점 | 적합한 환경 |
|---|---|---|---|---|
| Serial GC | 단일 스레드 GC | 구현이 단순, 메모리 사용량 적음 | STW 시간이 길어짐 | 작은 애플리케이션 |
| Parallel GC | 멀티 스레드 GC | 높은 처리량, 멀티코어 활용 가능 | STW 시간이 길 수 있음 | 서버 애플리케이션 |
| CMS GC | 동시 실행 GC | 응답 속도 향상 | 메모리 단편화 발생 가능 | 실시간 서비스 |
| G1 GC | Region 기반 GC | STW 시간 최소화, 대용량 Heap 지원 | Serial GC보다 복잡 | 대규모 시스템 |
| ZGC | 초저지연 GC | STW 시간 10ms 이하, 초대형 Heap 지원 | JDK 11 이상에서만 사용 가능 | 금융, 게임 서버 |
| Shenandoah GC | 초고속 GC | 낮은 레이턴시, 빠른 GC 수행 | 상대적으로 최신 기술 | 실시간 애플리케이션 |
→ Java 8까지는 Parallel GC가 기본이었고, Java 9부터는 G1 GC가 기본 GC야!
→ JDK 11 이상에서는 ZGC, Shenandoah GC 같은 초저지연 GC를 선택할 수도 있어!
Java 8에서는 기본 GC 알고리즘으로 Parallel GC(병렬 GC)를 사용함
또한 JVM은 세대별 GC(Generational GC) 방식을 적용해
Young Generation과 Old Generation을 다르게 관리함
Java 8의 기본 GC 알고리즘: Parallel GC
Java 8의 GC 구조: 세대별 GC(Generational GC)
/* Java 8의 Heap 메모리 구조 */
┌───────────────────────────────────┐
│ Heap Memory │
├────────────────────┬──────────────┤
│ Young Gen │ Old Gen │
│ (Eden + S0 + S1) │ (Tenured) │
└────────────────────┴──────────────┘
→ 각 영역에서 GC가 수행되는 방식이 다름!
Heap 메모리를 Young Generation과 Old Generation으로 나누는 이유는
GC 성능 최적화 때문이다 ! 이렇게 나누면 객체의 생존 기간을
고려한 GC 전략을 적용할 수 있어서 전체적인 성능이 향상된다 !
1. 객체의 생존 기간이 다름
대부분의 객체는 생성된 후 금방 사라짐
→ 객체의 생존 기간을 고려하여 GC를 최적화하기 위해
Heap을 Young Gen과 Old Gen으로 나눔!
2. Young Generation과 Old Generation의 차이점
Young Generation
Old Generation
→ Young GC는 자주 실행되지만 빠름, Old GC는 실행 빈도는 낮지만 오래 걸림!
3. GC 성능 최적화를 위해 세대별 GC 전략 사용
Young Generation GC (Minor GC)
Young Gen에서는 대부분의 객체가 금방 필요 없어지기 때문에,
GC를 자주 실행하여 빠르게 제거
GC 방식: "Copying GC" → 살아남은 객체만
Survivor 영역으로 이동 (빠른 수행 가능!)
Young GC 과정
→ Young GC는 빠르게 수행되어 애플리케이션 성능에 큰 영향을 주지 않음!
→ Old GC는 실행 속도가 느리므로 Full GC 발생을 최소화하는 것이 중요!
4. Young Generation과 Old Generation으로 나눴을 때의 장점
GC는 세대별 전략을 활용하여 최적화된 방식으로 실행됨!
| 버전 | 기본 GC 알고리즘 | 특징 |
|---|---|---|
| Java 8 | Parallel GC | Throughput(처리량) 최적화, 멀티 스레드 GC |
| Java 11 | G1 GC | STW(Stop-the-World) 최소화, 대용량 Heap 최적화 |
→ Java 8은 성능(처리량)에 초점, Java 11은 STW 시간 최소화에 초점!
┌──────────────────────────────────────────────┐
│ Heap Memory │
├───────────┬──────────┬────────────┬──────────┤
│ Region │ Region │ Region │ Region │
│ (Eden) │ (Old) │ (Survivor)│ (Old) │
├───────────┴──────────┴────────────┴──────────┤
│ Young Gen | Old Gen | Humongous │
└──────────────────────────────────────────────┘
Java 8에서는 기본적으로 Parallel GC가 사용됐지만
Java 9부터 기본 GC가 G1 GC(Garbage First GC)로 변경됐고 Java 11에서도 유지됐음
그 이유는 STW(Stop-the-World) 시간을 줄이고 대규모 Heap을 효과적으로 관리하기 위해서 !