JVM 한눈에 보기: 클래스 로딩부터 메모리 구조까지
들어가며
Java를 공부하다 보면 가장 자주 듣는 단어 중 하나가 JVM이다.
하지만 막상 JVM을 설명하려고 하면 "자바 코드를 실행해 주는 가상 머신" 정도에서 멈추기 쉽다.
실제로는 그 안에서 꽤 많은 컴포넌트가 협력한다.
클래스를 메모리에 올리고, 바이트코드를 검증하고, 메서드를 실행하고, 객체를 할당하고, 더 이상 필요 없는 객체를 회수하는 과정까지 모두 JVM 내부에서 벌어진다.
이 글은 JVM 전체를 처음부터 끝까지 한 번에 정리하기보다, 아래 질문에 답하는 데 집중한다.
- Java 코드는 어떻게 실행되는가
- 클래스는 언제, 어떻게 로드되는가
- JVM 메모리는 어떻게 나뉘는가
- 객체와 메서드 실행은 메모리 어디에서 일어나는가
1) JVM은 무엇을 하는가
JVM(Java Virtual Machine)은 Java 바이트코드를 실행하는 런타임이다.
핵심 역할은 크게 네 가지다.
- 클래스를 로드한다
- 런타임 메모리를 관리한다
- 바이트코드를 실행한다
- 사용하지 않는 객체를 정리한다
구조를 아주 단순화하면 아래처럼 볼 수 있다.

여기서 중요한 포인트는 JVM이 단순히 "코드 실행기"가 아니라,
로딩, 실행, 메모리 관리까지 포함한 전체 런타임 시스템이라는 점이다.
2) Java 코드는 어떻게 실행되는가
Java 코드는 바로 기계어로 떨어지지 않는다.
먼저 javac가 소스 코드를 바이트코드로 컴파일하고, 그 바이트코드를 JVM이 해석하거나 JIT 컴파일해서 실행한다.

핵심 특징은 아래와 같다.
- Java 코드는 OS/CPU별 기계어로 직접 컴파일되지 않는다.
- 중간 산출물인 바이트코드로 변환된다.
- JVM이 이 바이트코드를 각 환경에서 실행한다.
이 구조 덕분에 Java는 흔히 말하는 Write Once, Run Anywhere 특성을 가진다.
물론 정확히 말하면 "어디서나 동일하게 실행된다"는 뜻이 아니라,
해당 플랫폼용 JVM만 있다면 같은 바이트코드를 재사용할 수 있다는 의미에 가깝다.
3) ClassLoader
3-1) 왜 필요한가
JVM은 프로그램 시작 시 모든 클래스를 한꺼번에 다 올리지 않는다.
필요한 시점에 클래스를 메모리에 적재한다. 이를 보통 lazy loading이라고 부른다.
예를 들어 아래 코드를 보자.
public class Main {
public static void main(String[] args) {
User user = new User();
}
}
실행 흐름을 단순화하면 이렇다.
- JVM 시작
Main.class로드main()실행User타입이 실제로 필요해짐User.class로드
즉, ClassLoader는 필요한 클래스를 찾아 읽고 JVM이 사용할 수 있는 형태로 적재하는 관문이다.
실제 JVM 구현에서는 이 로딩 과정이 보통 parent delegation model과 함께 설명된다.
다만 이 글은 전체 흐름을 먼저 잡는 것이 목적이므로, 자세한 내용은 맨 아래 부록에서 따로 정리한다.
3-2) 클래스 로딩은 세 단계로 진행된다
클래스 로딩은 보통 아래 세 단계로 설명한다.

3-2-1) Loading
.class 파일을 읽어서 JVM 메모리에 적재한다.
이 단계에서 클래스 메타정보가 JVM의 Method Area에 올라간다.
HotSpot JVM 기준으로는 이 영역이 Metaspace로 구현된다.
여기서 한 가지는 정확히 구분해 두는 게 좋다.
Method Area: JVM 명세상의 개념Metaspace: HotSpot JVM의 실제 구현 방식
둘을 완전히 같은 말처럼 써도 초급 설명에서는 통하지만, 엄밀히는 다르다.
3-2-2) Linking
Linking은 다시 세 단계로 나뉜다.
- Verification
- Preparation
- Resolution
Verification
바이트코드가 JVM 명세를 어기지 않았는지 검증한다.
여기서는 단순히 "코드가 이상한가"만 보는 것이 아니라, 클래스 파일이 JVM이 처리할 수 있는 구조인지 여러 층위에서 확인한다.
- 파일 포맷이 유효한가
- 클래스 메타데이터가 올바른가
- 바이트코드 명령이 규칙을 어기지 않는가
- 심볼릭 레퍼런스가 말이 되는 참조인가
예를 들어:
CAFEBABE매직 넘버를 포함한 class file format이 깨졌는지- 부모 클래스, 인터페이스, 필드/메서드 정보 같은 metadata가 비정상적인지
- 피연산자 스택 사용이나 타입 흐름이 잘못된 bytecode가 들어왔는지
- 존재하지 않는 클래스/메서드/필드를 가리키는 symbolic reference가 있는지
이런 문제가 있으면 JVM은 클래스 로딩 단계에서 오류를 발생시켜 잘못된 코드를 실행하지 않도록 막는다.
Preparation
static 필드에 필요한 메모리를 할당하고 기본값으로 초기화한다.
중요한 점은 이 시점에는 아직 개발자가 작성한 초기값 대입 코드가 실행되지 않는다는 것이다.
예를 들어:
class Sample {
static int value = 10;
}
Preparation 단계에서는 value를 위한 메모리가 잡히고 일단 기본값 0으로 준비된다.
실제 10 대입은 다음 Initialization 단계에서 수행된다.
Resolution
심볼릭 레퍼런스를 실제 참조로 바꾸는 과정이다.
쉽게 말해,
"이름으로만 알고 있던 클래스/메서드/필드"를 JVM이 실제 메모리상의 연결 대상으로 해석하는 과정이라고 보면 된다.
3-2-3) Initialization
이 단계에서 클래스 초기화 코드가 실행된다.
대표적으로 static 블록과 static 필드 초기화가 여기서 처리된다.
class Test {
static {
System.out.println("class init");
}
}
이 코드는 내부적으로 <clinit>() 메서드 형태로 컴파일되어 실행된다.
4) JVM 메모리 구조: Runtime Data Areas
JVM 메모리는 크게 스레드 간 공유 영역과 스레드별 전용 영역으로 나눌 수 있다.

정리하면 아래와 같다.
- Heap: 객체 저장
- Method Area: 클래스 메타정보, 런타임 상수 풀, static 관련 정보
- Java Stack: 메서드 호출 프레임 저장
- PC Register: 현재 실행 중인 명령 위치 추적
- Native Method Stack: JNI 등 네이티브 호출 처리
4-1) 스펙 관점과 구현체 관점은 구분해야 한다
여기서 한 가지를 먼저 분리해서 이해하면 뒤가 훨씬 덜 헷갈린다.
JVM 명세가 직접 정의하는 메모리 구조는 Heap, Method Area, Java Stack, PC Register, Native Method Stack 같은 Runtime Data Areas다.
반면 Eden, Survivor, Old, TLAB 같은 용어는 JVM 스펙 자체의 공식 구분이 아니라, HotSpot JVM을 포함한 구현체가 Heap을 실제로 운영할 때 자주 등장하는 개념이다.
즉, 아래처럼 두 층을 나눠서 이해하는 것이 정확하다.
- 명세 수준:
Heap,Method Area,Java Stack,PC Register,Native Method Stack - 구현체 수준:
Eden,Survivor,Old,TLAB
개발자들이 메모리를 설명할 때 Eden 이야기를 자주 하는 이유는, 실제 성능 튜닝과 GC 관찰에서 이 구현체 관점의 구분을 많이 접하기 때문이다.
즉, 메모리 설명에서 Heap과 Eden이 함께 등장해도 서로 같은 레벨 개념은 아니다.
이 차이도 아래 부록에서 한 번 더 정리한다.
4-2) Heap: 객체는 어디에 사는가
Heap은 객체 인스턴스가 저장되는 대표적인 메모리 영역이다.
User user = new User();
위 코드에서 new User()로 생성된 객체는 일반적으로 Heap에 올라간다.
Heap 자체는 JVM 명세상의 영역이고, 실제 구현체에서는 이 Heap을 세대별로 운영하는 경우가 많다.
대표적인 설명 방식이 아래의 Eden - Survivor - Old 구분이다.

기본 흐름은 다음과 같다.
- 새 객체는 보통 Eden에 할당된다.
- Young GC 이후 살아남은 객체는 Survivor를 거친다.
- 오래 살아남은 객체는 Old 영역으로 승격된다.
즉, "대부분의 객체는 금방 죽는다"는 가정 위에 GC 전략이 설계되어 있다.
4-3) Java Stack: 메서드 실행은 어떻게 관리되는가
Stack은 메서드 실행을 위한 메모리 영역이다.
메서드가 호출되면 Stack Frame이 생성되고, 메서드가 끝나면 프레임이 제거된다.

Stack Frame 안에는 보통 이런 정보가 들어 있다.
- Local Variable Table: 매개변수와 지역 변수
- Operand Stack: 바이트코드 연산용 스택
- Dynamic Linking: 메서드/필드 참조 연결 정보
- Return Address: 호출 복귀 위치
예를 들어 아래 메서드를 보자.
int sum(int a, int b) {
return a + b;
}
이런 메서드는 바이트코드 수준에서 대략 이렇게 동작한다.
iload_1
iload_2
iadd
ireturn
흐름은 단순하다.
a를 Operand Stack에 올린다.b를 Operand Stack에 올린다.iadd로 두 값을 더한다.ireturn으로 결과를 반환한다.
즉, JVM은 레지스터 기반이 아니라 스택 기반 바이트코드 머신이라는 점이 중요하다.
4-4) PC Register: 지금 어디를 실행 중인가
PC Register는 현재 실행 중인 JVM 명령의 위치를 가리킨다.
각 스레드는 자신만의 PC Register를 가진다.
그래서 멀티스레드 환경에서도 스레드별로 서로 다른 실행 위치를 독립적으로 추적할 수 있다.

짧게 말하면:
- Stack은 "무엇을 실행 중인가"를 담고
- PC Register는 "어디까지 실행했는가"를 가리킨다
4-5) 객체 메모리 레이아웃은 어떻게 생겼는가
JVM 내부에서 객체는 단순히 "필드 값 덩어리"만으로 존재하지 않는다.
객체 헤더와 메타 정보가 함께 붙는다.

일반적으로 객체는 아래 요소로 구성된다.
- Mark Word: 락 상태, 해시, GC age 등 런타임 정보
- Klass Pointer: 어떤 클래스의 인스턴스인지 가리키는 포인터
- Instance Fields: 실제 필드 데이터
- Padding: 메모리 정렬을 맞추기 위한 여분 공간
다만 객체 헤더 세부 구조는 JVM 구현과 옵션에 따라 달라질 수 있다.
예를 들어 biased locking 관련 설명은 JDK 버전에 따라 달라지므로 항상 "절대 고정 구조"처럼 외우면 안 된다.
4-6) 객체 할당 최적화: 항상 Heap만 쓰는 것은 아니다
객체 생성 시 JVM은 단순히 Heap에만 기계적으로 할당하지 않는다.
실행 성능을 높이기 위해 여러 최적화를 적용할 수 있다.
4-6-1) Escape Analysis
객체가 메서드 밖으로 탈출하지 않는다고 판단되면, JVM은 더 공격적인 최적화를 시도할 수 있다.
- 스칼라 치환: 객체를 통째로 만들지 않고, 내부 필드를 개별 값처럼 분해해서 다루는 최적화
- 동기화 제거: 다른 스레드와 공유되지 않는 객체라면 synchronized 같은 락 비용을 없애는 최적화
- 스택 할당처럼 보이는 최적화: 객체가 외부로 escape 하지 않으면 Heap 할당 자체를 줄이거나 없애서, 결과적으로 Stack에만 있던 것처럼 동작하게 만드는 최적화
여기서 주의할 점은 흔히 말하는 "Heap 대신 Stack에 무조건 할당된다"는 설명이 과장일 수 있다는 것이다.
Escape Analysis는 가능성을 열어 주는 분석이지, 항상 스택 할당을 보장하는 규칙은 아니다.
4-6-2) TLAB
멀티스레드 환경에서 모든 스레드가 Eden 영역에 동시에 객체를 할당하려 하면 경쟁이 심해질 수 있다.
이를 줄이기 위해 JVM은 TLAB(Thread Local Allocation Buffer)를 사용한다.

즉, 각 스레드가 Eden의 일부를 자기 전용 버퍼처럼 받아서 빠르게 객체를 할당하는 방식이다.
5) Execution Engine은 무엇을 할까
Execution Engine은 바이트코드를 실제로 실행하는 영역이다.
여기에는 다음 주제가 연결된다.
- Interpreter
- JIT Compiler
- Profiling
- HotSpot Detection
- Tiered Compilation
- Safepoint
- Deoptimization
이번 글은 JVM 전체 그림을 잡는 데 집중했기 때문에 여기서는 개요만 남긴다.
다음 글에서는 Execution Engine 내부가 왜 성능에 직접적인 영향을 주는지 더 깊게 다루면 좋다.
마치며
JVM을 공부할 때 가장 중요한 건 개별 용어를 따로 외우는 것이 아니다.
클래스 로딩, 메모리 구조, 메서드 실행, 객체 할당이 한 흐름으로 연결되어 있다는 점을 이해하는 것이다.
정리하면:
- ClassLoader는 클래스를 필요할 때 메모리에 올린다.
- Runtime Data Areas는 실행 중 필요한 데이터를 영역별로 관리한다.
- Stack은 메서드 호출을, Heap은 객체 생성을 담당한다.
- Execution Engine은 바이트코드를 실제로 실행한다.
- GC는 Heap을 중심으로 더 이상 필요 없는 객체를 정리한다.
한 줄로 요약하면 아래와 같다.
JVM은 단순히 Java 코드를 실행하는 엔진이 아니라, 클래스 로딩부터 메모리 관리와 실행 최적화까지 담당하는 전체 런타임 시스템이다.
부록
부록 A) Parent Delegation Model
ClassLoader를 조금 더 깊게 공부하면 보통 Bootstrap -> Platform -> Application 계층과 함께 parent delegation model이 등장한다.
기본 계층은 아래처럼 이해하면 된다.
Bootstrap ClassLoader:java.lang,java.util같은 핵심 JDK 클래스를 로드하는 최상위 로더Platform ClassLoader: Java 플랫폼 모듈 전반을 로드하는 로더Application ClassLoader: 일반적으로 우리가 작성한 애플리케이션 클래스패스를 로드하는 로더
즉, 평소 개발자가 직접 다루는 대부분의 비즈니스 코드는 보통 Application ClassLoader를 통해 로드된다.
핵심 아이디어는 단순하다.
- 어떤 클래스를 로드해야 할 때
- 현재 로더가 바로 읽지 않고
- 먼저 부모 로더에게 로드를 위임한다
- 부모가 못 찾으면 그때 자식 로더가 직접 로드한다
흐름을 단순화하면 아래와 같다.

이 모델을 쓰는 이유는 크게 세 가지다.
- 핵심 JDK 클래스를 신뢰 가능한 로더가 우선 로드하게 하기 위해
- 클래스 중복 로딩 충돌을 줄이기 위해
- 로딩 책임을 계층적으로 분리하기 위해
예를 들어 java.lang.String 같은 클래스는 애플리케이션 로더가 자기 멋대로 다시 정의하는 것이 아니라, 상위 로더 체인에서 우선 처리된다.
물론 모든 경우가 완전히 이 규칙만 따르는 것은 아니다.
애플리케이션 서버, OSGi, 프레임워크 환경에서는 필요에 따라 커스텀 로더 전략이 들어갈 수 있다.
그래서 parent delegation은 "가장 기본이 되는 로딩 원칙"으로 이해하는 것이 정확하다.
부록 B) JVM 스펙과 구현체는 어떻게 다른가
JVM을 공부할 때 자주 헷갈리는 이유 중 하나는,
명세에 있는 개념과 실제 HotSpot JVM 구현에서 자주 보는 용어가 섞여서 설명되기 때문이다.
가장 대표적인 예가 아래다.
| 구분 | 명세 수준 개념 | 구현체에서 자주 보는 용어 |
|---|---|---|
| 클래스 메타정보 영역 | Method Area | Metaspace |
| 객체 저장 영역 | Heap | Eden / Survivor / Old |
| 스레드별 실행 영역 | Java Stack | Stack Frame, Local Variable Table |
| 객체 할당 최적화 | 명세에 직접 없음 | TLAB, Escape Analysis |
즉, Heap과 Eden은 같은 말이 아니다.
Heap: JVM 명세가 정의하는 메모리 영역Eden: 구현체가 Heap을 실제로 운영할 때 쓰는 세부 구분
비슷하게:
Method Area: JVM 명세 개념Metaspace: HotSpot 구현 용어
이 구분이 중요한 이유는,
책이나 블로그마다 어떤 글은 명세 중심으로 설명하고 어떤 글은 HotSpot 동작 중심으로 설명하기 때문이다.
실무에서는 구현체 용어를 더 자주 접한다.
- GC 로그를 볼 때는 Eden, Survivor, Old
- OOM을 볼 때는 Metaspace
- 성능 최적화를 볼 때는 TLAB, Escape Analysis
반대로 개념을 처음 잡을 때는 명세 수준 구조부터 보는 편이 더 덜 헷갈린다.
그래서 JVM 학습 순서는 보통 아래처럼 가져가면 좋다.
- 먼저 명세 수준 구조를 이해한다.
- 그다음 HotSpot 같은 구현체가 이를 어떻게 실제로 운영하는지 본다.
- 마지막에 GC, JIT, 메모리 튜닝처럼 런타임 최적화 주제로 확장한다.
부록 C) JDK, JRE, JVM은 무엇이 다른가
세 용어는 자주 같이 나오지만 같은 말은 아니다.
JVM: Java 바이트코드를 실행하는 가상 머신JRE: JVM + 표준 라이브러리 + 실행에 필요한 런타임 구성JDK: JRE + 개발 도구(javac,javadoc,jstack,jmap등)
즉:
- 프로그램을 "실행"하는 핵심은 JVM
- Java 프로그램이 "돌아가도록 묶인 실행 환경"은 JRE
- 개발자가 "코드를 만들고 컴파일하고 진단"하는 데 필요한 것은 JDK
다만 현대 Java에서는 예전처럼 JRE를 별도 설치 패키지로 강하게 구분하지 않는 경우가 많다.
그래서 실무에서는 보통 "JDK 설치"를 기본으로 이야기하고, 그 안에 런타임까지 포함된다고 이해하는 편이 자연스럽다.
부록 D) JNI와 Native Method Stack
본문에서 Native Method Stack이 등장했기 때문에, 이 영역이 왜 필요한지도 같이 보면 좋다.
JNI(Java Native Interface)는 Java 코드가 C/C++ 같은 네이티브 코드와 상호작용할 수 있게 해 주는 인터페이스다.
대표적인 상황은 아래와 같다.
- 운영체제 기능을 직접 호출해야 할 때
- 성능이 중요한 저수준 라이브러리를 연결할 때
- 기존 네이티브 라이브러리를 Java 애플리케이션에서 재사용할 때
예를 들어 Java 코드에서 native 메서드를 선언하면 실제 구현은 JVM 바깥의 네이티브 코드에 존재할 수 있다.
public class NativeExample {
public native int sum(int a, int b);
}
이때 JVM은 Java 세계와 Native 세계를 연결해야 하고, 그 과정에서 Native Method Stack이 관련 실행 맥락을 처리한다.
아주 단순화하면 흐름은 이렇다.

Native Method Stack은 Java 바깥의 네이티브 실행 경로와 연결될 때 등장하는 영역이다.
부록 E) Runtime Constant Pool과 String Pool
Method Area를 이야기했다면 같이 헷갈리기 쉬운 개념이 Runtime Constant Pool과 String Pool이다.
둘은 관련은 있지만 같은 것은 아니다.
Runtime Constant Pool
각 .class 파일에는 constant pool 정보가 들어 있다.
클래스가 로드되면 이 정보가 런타임에서 사용할 수 있는 형태로 올라가는데, 이것을 Runtime Constant Pool이라고 부른다.
여기에는 보통 아래와 같은 참조/상수 정보가 포함된다.
- 문자열 리터럴 정보
- 클래스/메서드/필드 심볼릭 레퍼런스
- 숫자 상수
즉, Resolution 단계에서 심볼릭 레퍼런스를 실제 참조로 연결할 때도 이 정보가 중요한 역할을 한다.
String Pool
String Pool은 문자열 재사용을 위한 저장소라고 이해하면 된다.
예를 들어 아래 두 코드는 다르게 동작할 수 있다.
String a = "hello";
String b = "hello";
String c = new String("hello");
a와b는 같은 문자열 리터럴을 재사용할 수 있다c는 별도 객체를 만들 수 있다
intern()을 사용하면 문자열을 풀에 맞춰 재사용하도록 만들 수 있다.
중요한 점은 Runtime Constant Pool과 String Pool이 같은 개념이 아니라는 것이다.
Runtime Constant Pool: 클래스 단위 상수/참조 정보String Pool: 문자열 재사용 메커니즘
또한 구현 디테일은 JDK 버전에 따라 달라질 수 있다.
예를 들어 String Pool의 실제 메모리 배치는 버전별로 차이가 있었다.
부록 F) .class 파일은 무엇으로 이루어지는가
Verification에서 "파일 포맷", "메타데이터", "심볼릭 레퍼런스"를 언급했다면, .class 파일 구조도 아주 짧게 보는 편이 이해에 도움이 된다.
대표 구조는 아래와 같다.
- Magic Number
- Version
- Constant Pool
- Access Flags
- This Class / Super Class
- Interfaces
- Fields
- Methods
- Attributes
흐름을 단순화하면:

여기서:
Magic Number는 파일이 class file 형식인지 확인하는 시작점이다Constant Pool은 참조와 상수 정보를 담는다Methods에는 실제 바이트코드가 담긴다Attributes에는 추가 메타데이터가 들어간다
즉, Verification은 단순히 소스 코드를 보는 것이 아니라 이 .class 파일 구조 자체가 JVM 규칙에 맞는지를 확인하는 과정이기도 하다.
부록 G) 메모리 영역별 대표 오류
JVM 메모리를 공부할 때는 "각 영역이 어떤 역할을 하는가"와 함께 "문제가 나면 어떤 오류로 보이는가"도 같이 보면 이해가 빨라진다.
대표 예시는 아래와 같다.
| 영역/주제 | 대표 오류/현상 | 예시 |
|---|---|---|
| Java Stack | StackOverflowError |
재귀가 너무 깊을 때 |
| Heap | OutOfMemoryError: Java heap space |
객체를 너무 많이 만들 때 |
| Metaspace | OutOfMemoryError: Metaspace |
클래스를 과도하게 로드할 때 |
| Direct Memory | OutOfMemoryError: Direct buffer memory |
NIO direct buffer를 과도하게 사용할 때 |
이 표를 보면 알 수 있듯이, OOM이 난다고 해서 모두 같은 원인은 아니다.
어느 영역이 부족한지에 따라 접근 방식도 달라진다.
부록 H) Class Identity는 클래스 이름만으로 결정되지 않는다
JVM에서 클래스는 단순히 "클래스 이름"만으로 식별되지 않는다.
보통 클래스의 정체성(class identity)은 클래스 이름 + 그 클래스를 정의한 ClassLoader의 조합으로 이해한다.
즉, 같은 com.example.User라도 다른 로더가 각각 로드했다면 JVM 입장에서는 서로 다른 타입이 될 수 있다.
예를 들어:
com.example.User를 로더 A가 로드- 같은
com.example.User를 로더 B가 다시 로드
이 둘은 이름은 같아도 JVM 내부에서는 다른 클래스로 취급될 수 있다.
그래서 아래 같은 문제가 발생할 수 있다.
ClassCastExceptioninstanceof결과가 기대와 다름- 프레임워크 프록시나 플러그인 구조에서 타입 충돌
한 줄로 요약하면 이렇다.
JVM에서 타입 동일성은 클래스 이름만이 아니라, 어떤 ClassLoader가 정의했는지도 포함한다.
부록 I) Spring Boot DevTools는 왜 빠르게 다시 뜨는가
위의 class identity 개념은 Spring Boot DevTools의 재시작 원리를 이해할 때도 매우 중요하다.
DevTools는 보통 클래스 로더를 크게 두 층으로 나눈다.
- 변경이 거의 없는 라이브러리들은
base classloader - 자주 바뀌는 애플리케이션 클래스들은
restart classloader
아이디어는 단순하다.
- 애플리케이션 코드가 수정된다
- IDE가 변경 클래스를 다시 컴파일해 클래스패스 산출물을 갱신한다
- DevTools가 이를 감지한다
- 기존 restart classloader를 버리고 새 restart classloader를 만든다
- 애플리케이션 컨텍스트를 다시 띄운다
즉, JVM 전체를 완전히 종료했다가 다시 띄우는 것이 아니라,
변경 가능성이 높은 클래스들만 새로운 로더로 다시 읽어들이는 방식에 가깝다.
이를 단순화하면 아래처럼 볼 수 있다.

이 구조 덕분에 매번 전체 JVM 프로세스를 처음부터 부팅하는 것보다 빠르게 개발 피드백을 받을 수 있다.
다만 정확히 말하면 "소스만 바꾸면 아무 작업 없이 바로 반영"되는 것은 아니다.
보통은 IDE의 자동 컴파일이나 빌드 산출물 갱신이 먼저 일어나고, DevTools는 그 변경된 클래스패스를 감지해 재시작을 수행한다.
그리고 classloader가 달라지면 class identity도 달라질 수 있기 때문에, 캐시/정적 상태/직렬화/리플렉션 경계에서는 미묘한 문제가 생길 수 있다.
그래서 DevTools는 매우 편리하지만, 내부적으로는 꽤 classloader-sensitive한 기능이라고 볼 수 있다.
'CS > JAVA' 카테고리의 다른 글
| 자바 컬렉션 한눈에 보기 4편: HashSet과 TreeSet, 중복 판정의 기준 (0) | 2026.03.19 |
|---|---|
| 자바 컬렉션 한눈에 보기 3편: ArrayList부터 Iterator, fail-fast까지 (0) | 2026.03.19 |
| 자바 컬렉션 한눈에 보기 1편: 해시부터 hashCode, equals, 충돌까지 (0) | 2026.03.19 |
| JVM 한눈에 보기 GC부터 ZGC까지: 큰 그림, 내부 구조, 구현체 비교, ZGC 심화 (1) | 2026.03.12 |
| JVM 한눈에 보기: Interpreter, JIT, Inline, Safepoint까지 (0) | 2026.03.11 |