자바 코드를 실행하면 .class 파일이 JVM 메모리에 올라갑니다. 이 과정을 클래스 로딩(Class Loading) 이라고 부릅니다.
대부분의 자바 개발자는 이 과정이 "알아서 된다"고 넘어가는데, 막상 ClassNotFoundException이나 NoClassDefFoundError를 마주치거나, 리플렉션이나 동적 로딩을 다루게 되면 내부 동작을 모른다는 게 발목을 잡습니다.
이 글에서는 클래스 로딩의 두 단계인 Loading과 Linking이 JVM 내부에서 어떻게 동작하는지 살펴봅니다.
클래스 로딩의 전체 흐름
클래스 로딩은 Loading → Linking → Initialization 순서로 진행됩니다. Linking은 다시 세 단계(Verification → Preparation → Resolution)로 나뉩니다.

이 글에서는 Loading과 Linking을 다룹니다. Initialization은 그 결과물인 InstanceKlass와 Class 객체 구조와 함께 2편에서 이어갑니다.
Loading — .class 파일을 메모리에 올리기
Loading의 결과물
Loading 단계는 .class 파일(바이트코드)을 찾아서 JVM 메모리에 올리는 과정입니다. 결과물로 두 가지가 만들어집니다.
- InstanceKlass — JVM 내부의 C++ 메타데이터 구조체 (Metaspace에 위치)
- java.lang.Class 객체 — Java 힙에 올라가는 미러 객체
이 둘의 관계는 2편에서 자세히 다루고, 여기서는 Loading이 어떤 순서로 진행되는지에 집중합니다.
ClassLoader와 부모 위임 모델
클래스를 로드하는 주체는 ClassLoader입니다. JVM에는 기본적으로 세 계층의 ClassLoader가 있습니다.

- Bootstrap ClassLoader — JVM에 내장된 최상위 로더입니다.
java.*,javax.*같은 핵심 클래스를 담당합니다. - Platform ClassLoader — Java 9의 모듈 시스템과 함께 도입됐습니다. Java 8까지는 Extension ClassLoader라는 이름으로
jre/lib/ext하위 클래스를 담당했습니다. - Application ClassLoader — classpath에 있는 클래스, 즉 개발자가 작성한 클래스를 담당합니다.
클래스 로드 요청이 들어오면 JVM은 항상 부모에게 먼저 위임합니다. 부모가 클래스를 찾지 못할 때만 자식이 직접 로드를 시도합니다. 이를 부모 위임 모델(Parent Delegation Model) 이라고 합니다.
이 구조 덕분에 java.lang.String 같은 핵심 클래스를 개발자가 직접 만든 클래스로 덮어쓰는 것이 불가능합니다.
.class 파일을 찾고 나면
ClassLoader가 .class 파일을 찾으면 바이트배열로 읽어서 defineClass()를 호출합니다. 이 시점에 JVM 네이티브 레이어로 진입해서 ClassFileParser가 바이트코드를 파싱하고, InstanceKlass와 Class 객체가 생성됩니다.
클래스는 처음 필요할 때 로드됩니다 (Lazy Loading)
JVM은 클래스를 미리 전부 로드하지 않습니다.
new MyClass(),MyClass.CONSTANT참조,Class.forName("MyClass")호출 등 실제로 클래스가 필요해지는 시점에 처음 로드됩니다.
Linking — 로드된 클래스를 실행 가능한 상태로 만들기
Loading이 끝나면 바로 Linking이 시작됩니다. Linking은 세 단계로 이루어집니다.
1단계: Verification (검증)
로드한 바이트코드가 JVM 명세에 맞는지 검사하는 단계입니다. 외부에서 가져온 .class 파일이 손상되거나 악의적으로 조작되지 않았는지 확인하는 안전장치입니다.
주요 검사 항목은 다음과 같습니다.
- 파일 시작이
0xCAFEBABE인지 확인 (자바 클래스 파일 매직 넘버) - 타입 안전성 검사 —
int타입 변수에 객체 참조를 넣는 식의 위반이 없는지 - 스택 일관성 검사 — 메서드 실행 중 스택이 비정상적으로 넘치거나 부족하지 않은지
- 접근 제어 검사 —
private멤버를 외부에서 직접 접근하는 코드가 없는지
컴파일러(javac)가 만들어낸 정상적인 .class 파일은 거의 다 통과합니다. Verification이 부담스럽다면 JVM 옵션 -Xverify:none으로 끌 수 있지만, 외부 코드를 로드하는 환경에서는 보안상 위험합니다.
2단계: Preparation (준비)
static 필드를 위한 메모리를 할당하고, 기본값으로 초기화하는 단계입니다.
public class Foo {
static int count = 100; // Preparation: 0으로 초기화
static String name = "bar"; // Preparation: null로 초기화
}
여기서 주의할 점은 개발자가 작성한 초기값(100, "bar")은 아직 적용되지 않는다는 것입니다. 그 값은 Initialization 단계에서야 대입됩니다. Preparation은 오직 타입별 기본값만 설정합니다.
| 타입 | 기본값 |
int, long, short, byte |
0 |
boolean |
false |
float, double |
0.0 |
| 참조 타입 (Object 등) | null |
3단계: Resolution (해석)
바이트코드 안의 심볼릭 참조를 실제 메모리 참조로 교체하는 단계입니다. 대상은 클래스 참조뿐만 아니라 메서드, 필드 참조도 포함됩니다.
컴파일된 .class 파일 안에는 다른 클래스나 메서드를 가리키는 참조가 이름 문자열 형태로 들어가 있습니다. 예를 들어 Foo 클래스가 Bar 클래스를 사용한다면, 바이트코드에는 "com/example/Bar" 라는 문자열만 있습니다. Resolution은 이 문자열을 실제 Bar 클래스의 메모리 주소로 교체합니다.
"com/example/Bar" (상수 풀의 심볼릭 참조)
↓ Resolution
InstanceKlass* (실제 메모리 포인터)
Resolution은 반드시 Linking 시점에 전부 완료되지 않습니다. JVM 명세는 Resolution을 지연(Lazy)하는 것을 허용하고, HotSpot은 이를 적극 활용합니다. 심볼릭 참조는 해당 코드가 실제로 실행되는 순간에 resolve됩니다. 덕분에 Foo 클래스를 로드할 때 Foo가 참조하는 모든 클래스를 한꺼번에 로드하지 않아도 됩니다.
정리
[Loading]
ClassLoader.loadClass()
→ 부모 위임으로 클래스 탐색
→ .class 파일 발견
→ defineClass() → ClassFileParser
→ InstanceKlass 생성 + Class 객체 생성
[Linking]
Verification → 바이트코드 유효성 검사
Preparation → static 필드 기본값 초기화
Resolution → 심볼릭 참조 → 실제 참조 (Lazy)
[다음 단계]
Initialization → static 블록 실행, static 필드 실제 값 대입
Loading과 Linking을 거치면 클래스는 실행 준비가 거의 된 상태입니다. 이 과정의 결과물인 InstanceKlass와 Class 객체가 실제로 어떤 구조인지, 그리고 Initialization이 어떻게 동작하는지는 2편에서 다룹니다.
'Language > Java' 카테고리의 다른 글
| [Java] JVM 클래스 로딩 Deep Dive 3편 — ClassLoader 동작 원리와 예외 (0) | 2026.05.17 |
|---|---|
| [Java] JVM 클래스 로딩 Deep Dive 2편 — InstanceKlass, Class 객체, Initialization (0) | 2026.05.17 |
| [Java] CountDownLatch 완벽 정리 — 멀티스레드 동기화의 핵심 (0) | 2026.05.17 |
| [Java] Java Concurrent 패키지 - Concurrent Collections 완벽 정리 (0) | 2026.05.17 |
| [JAVA] Java Concurrent 패키지 - Atomic 자료형 완전 정리 (1) | 2026.05.14 |