
1편에서는 Java 코드가 바이트코드로 변환되고 JVM 위에서 실행되는 전체 흐름을 살펴봤습니다.
2편에서는 컴파일러가 정적 타입을 기준으로 무엇을 검사하고, 런타임의 JVM이 실제 객체를 기준으로 무엇을 결정하는지 정리했습니다.
3편에서는 Heap의 객체에서 Klass Pointer와 Class Metadata를 따라가며 실제 메서드가 선택되는 흐름까지 살펴봤습니다.
그렇다면 이제 실행의 시작점으로 돌아가 보겠습니다.
질문!! JVM은
.class파일을 발견한 순간부터 바로main()을 실행할 수 있을까요?
JVM은 필요한 클래스를 로드하고, 검증하고, 실행에 필요한 상태를 준비한 뒤 초기화를 수행합니다.
이번 글에서는 .class 파일이 JVM 안에서 실행 가능한 타입이 되는 과정만 집중해서 살펴보겠습니다. 그래서 위 질문에 대한 답은 "그렇지 않습니다" 입니다.
main()보다 먼저 시작되는 과정
우리가 CLI에서 다음 명령을 실행했다고 가정해 보겠습니다.
java Main
겉으로는 Main.main()이 바로 실행되는 것처럼 보이지만, JVM 내부에서는 먼저 Main 클래스를 사용할 수 있는 상태로 만들어야 합니다.

클래스의 생명주기를 크게 나누면 다음과 같습니다.
Loading
→ Linking
├─ Verification
├─ Preparation
└─ Resolution
→ Initialization
→ 사용
→ 조건을 만족하면 Unloading
이번 글의 핵심은 Loading → Linking → Initialization입니다.
ClassLoader는 무엇을 할까?
요약 : `new SpecificClass();` 같은 코드가 실행되면, JVM은 가장 먼저 디스크에 있는 SpecificClass.class 파일을 읽어 메모리(Method Area)에 로드시키는 것

→ “클래스에 로드(로딩-Loading)”한다라는 말은 ‘컴파일 시점’이 아니라, ‘런 타임’ 시점에 클래스가 최초로 사용되는 런타임 시점에 Class Loader가 동작되어, JVM의 Method Area(Metaspace)에 클래스 메타데이터를 생성하는 것입니다.
- new Child()로 힙 영역에 인스턴스, 객체(Object)를 할당하는게 아닙니다.
이때 ‘첫 시점’의 trigger는 메인 스레드가 Person p = new Child(); 코드 한줄을 실행할 때 .. ‘new’ 만이 트리거는 아닙니다.
new Child();
Child.staticMethod();
Child.staticField;
Class.forName("Child");
참고로 ClassLoader가 파일을 읽는 기능만 담당하는 것은 아닙니다.

메인스레드가 new Child()를 실행한다 → 런타임 때 위와 같은 로직으로 돌아간다는 것입니다.
| 단계 | 담당 |
|---|---|
| Loading | ClassLoader + JVM |
| Linking (Verification / Preparation / Resolution) | JVM (HotSpot 내부) |
| Initialization | JVM |
대표적인 ClassLoader

일반적인 Java 실행 환경에서는 다음과 같은 ClassLoader 계층을 볼 수 있습니다.
- Bootstrap ClassLoader:
java.lang,java.util처럼 Java 실행에 필요한 핵심 클래스 로드 - Platform ClassLoader: Java 플랫폼에서 제공하는 모듈과 라이브러리 담당
- Application ClassLoader: 일반적으로 classpath나 module path에 있는 애플리케이션 클래스 로드
우리가 작성한 Main.class, Member.class 같은 클래스는 대부분 Application ClassLoader를 통해 로드됩니다.
다만 그림의 화살표를 단순히 “부모가 자식을 호출한다”는 의미로 보면 안 됩니다.
실제 클래스 탐색에서는 부모에게 먼저 요청하는 parent delegation 전략이 일반적으로 사용됩니다.

이 방식은 애플리케이션이 임의로 만든 java.lang.String 같은 클래스로 Java 핵심 클래스를 쉽게 대체하지 못하도록 하고, 동일한 핵심 클래스가 중복 로드되는 문제를 줄여 줍니다.
1. Loading — 클래스 바이트를 JVM 내부 타입으로 만들기
먼저 JVM이 어떤 클래스를 로딩하게 되는지 코드부터 보겠습니다.
public class Main {
public static void main(String[] args) {
Member member = new Member("seunghyeon");
member.introduce();
}
}
class Member {
private final String name;
Member(String name) { this.name = name; }
void introduce() { System.out.println("안녕하세요. " + name + "입니다."); }
}
터미널에서 java Main 명령을 실행하면 Java 실행 환경이 시작되고, JVM은 실행 시작 클래스인 Main을 찾습니다.
이후 Main 클래스를 로딩하고 초기화한 뒤, public static void main(String[] args) 메서드를 호출합니다.
따라서 JVM은 곧바로 new Member()부터 실행하는 것이 아닙니다. 먼저 Main 클래스를 실행 가능한 상태로 준비하고 main()을 실행한 뒤, 그 안에서 new Member() 명령어를 만났을 때 Member 클래스가 필요한지 확인합니다.
Loading 단계에서는 ClassLoader가 Main.class, Member.class와 같은 클래스의 바이트 데이터를 찾아 JVM에 전달합니다.
JVM은 이를 바탕으로 각 클래스를 실행 중 사용할 수 있는 런타임 타입으로 준비합니다.

여기서 만들어진 class Metadata는 new Main()으로 만드는 인스턴스 객체가 아닙니다.
- 클래스 로딩:
Main이라는 타입 자체를 JVM이 사용할 수 있도록 준비하는 과정 - ‘new’ 객체 생성: 로딩된 타입을 바탕으로 Heap에 실제 인스턴스를 만드는 과정
이 로딩 과정에서 JVM은 클래스 이름, 부모 클래스, 필드와 메서드, Runtime Constant Pool 등의 정보를 담은 Class Metadata를 준비합니다.
Class Metadata의 내부 구조는 3편에서 자세히 살펴봤으므로, 여기서는 “클래스를 실행하는 데 필요한 런타임 타입 정보” 정도로만 이해하고 넘어가겠습니다.
JVM 명세상 Class Metadata는 Method Area에서 관리되며, HotSpot JVM에서는 이를 주로 Metaspace!에 구현합니다.
2. Linking — 실행해도 되는 클래스인지 준비하기
요약 : Linking은 메모리에 로드된 바이트코드(클래스)가 안전하게 실행될 수 있는지 검사하고, static 필드와 참조 정보, 기본 메모리 공간 준비(Preparation)
Loading이 끝났다고 해서 곧바로 클래스 초기화가 시작되는 것은 아닙니다.
JVM은 클래스가 안전하고 일관된 형태인지 검사하고, 실행에 필요한 메모리와 참조 정보를 준비합니다.

Verification — 바이트코드 검증
Verification 단계에서는 클래스 파일의 구조와 바이트코드가 JVM 규칙을 만족하는지 검사합니다.
예를 들어 다음과 같은 내용을 확인합니다.
- 클래스 파일 형식이 올바른가?
- Constant Pool의 구조가 유효한가?
- 명령어가 타입 규칙을 위반하지 않는가?
- Operand Stack을 잘못 사용하는 흐름은 없는가?
- 허용되지 않은 메모리 접근을 시도하지 않는가?
Java 소스가 javac로 정상 컴파일되었다고 해서 JVM이 모든 .class 파일을 무조건 신뢰하는 것은 아닙니다.
클래스 파일은 다른 컴파일러나 도구로 만들어질 수도 있고, 파일 자체가 변경될 수도 있기 때문입니다.
즉 Verification은 다음 질문에 답하는 단계입니다.
이 바이트코드는 JVM이 안전하게 실행할 수 있는 형태인가?
검증에 실패하면 VerifyError와 같은 오류가 발생할 수 있습니다.
.java
→ javac의 소스 코드 검사
→ .class 생성
→ JVM의 Bytecode Verification
→ 실행
Preparation — static 필드에 기본값 할당
요약 : Preparation은 static 필드를 위한 메모리를 확보하고, 0, false, null 같은 기본값을 먼저 넣어 두는 단계
물론 클래스에 static 필드가 하나도 없으면, static 변수를 위해 메모리 공간 찾고 → 할당할 필요가 없습니다.
이 단계는 스킵되거나 아주 간단하게 수행될수도 있습니다.
Preparation 단계에서는 클래스 변수, 즉 static 필드를 위한 메모리가 준비되고 기본값이 할당됩니다.
class Counter {
static int count = 100;
static String name = "counter";
}
이 시점의 개념적인 상태는 다음과 같습니다.
- count = 0
- name = null
아직 코드에 작성한 100, "counter"가 대입되는 단계는 아닙니다.

여기서 중요한 구분은 다음과 같습니다.
- Preparation: 타입별 기본값을 준비 (일단 디폴트로 값을 넣어줌)
- Initialization: 개발자가 작성한 초기화 코드 실행 (개발자가 코딩한 명시적인 초기값 100, “counter” 대입됨
다만 static final 상수 중 ConstantValue 속성으로 표현되는 컴파일 타임 상수는 Preparation 과정에서 상수값이 설정될 수 있습니다.
→ ConstantValue 이거는 컴파일러가 .java 파일을 .class 파일로 바꿀 때 이런 단순 상수값들은 변하지 않는다는 것을 알고 있어서, 바이트코드 파일(class file) 내부에 ConstantValue라는 특별한 속성(명세)로 값들을 박아둠.
// 기본형(Primitive type) 혹은 String타입이면서도 static final로 선언된 상수
static final int MAX_COUNT = 100;
따라서 모든 static 필드가 반드시 “기본값을 거친 뒤 Initialization에서 명시값을 받는다”고 단정하면 정확하지 않습니다.
Q. 왜 클래스 멤버 변수는 자동 초기화?
• 메모리 구조와 재사용성: 클래스 변수나 인스턴스 변수는 객체나 클래스 영역(Method Area / Heap)에 넓게 살아남아 프로그램 전반에 영향을 줌.
값이 비어있으면(쓰레기 값이 들어가 있으면) 예측 불가능한 버그가 생기기 쉽기 때문에, JVM이 안전하게 기본값(0 등)으로 깔끔하게 채워두는 것
Q. 그럼 왜 함수 안 지역 변수는 자동 초기화 안해줄까요? (컴파일 에러: Variable 'xxx' might not have been initialized)
- 스택(Stack) 메모리의 특성: 지역 변수는 메서드가 호출될 때 스택(Stack) 메모리에 생성되었다가 메서드가 끝나면 순식간에 사라짐
- 성능(속도): 스택은 메서드가 실행될 때 엄청나게 자주, 빠르게 생겼다 사라지는 공간입니다. 여기에 자바가 일일이 기본값을 채워 넣는 초기화 작업을 거치면 성능에 낭비가 생깁니다.
- 실수 방지 (가장 중요): 지역 변수는 개발자가 메서드 안에서 로직을 수행하기 위해 잠시 쓰는 임시 값입니다. 만약 자바가 임의로
0같은 기본값을 넣어주었는데, 개발자가 값을 대입하는 것을 깜빡했다면? 프로그램은 에러 없이 돌아가지만0이라는 잘못된 값 때문에 치명적인 로직 오류(예: 0으로 나누기, 잘못된 계산 등)가 발생할 수 있음. - "지역 변수는 네가 직접 명확하게 초기화하고 써라! 안 그러면 아예 컴파일도 안 해주마" 하고 엄격하게 막아둠
추가적으로 OS적인 관점에서 설명을 좀 드리면,,
스택 영역은 stack frame 사이즈가 컴파일 시점에 아 이 함수는 몇 바이트가 필요하다 거의 계산되어 있습니다.
지역 변수 개수, 타입 등 코드로 고정되어 있기 때문. (속도 빠르지만, 공간이 유연하지 못하고 크기가 고정되어 있음)
반면 힙 영역에 할당되는 new 인스턴스는 이 객체가 메모리를 얼마나 차지할지는 실행해 봐야(런타임) 알수 있습니다. 런타임시에 힙 영역은 메모리 단편화(fragmentation)가 일어나게 됩니다.
그래서 런타임 시 슉~ 하고 "1. 맨 위에 추가"로 올리는 스택과 달리
힙 영역에서는 1. new 인스턴스 사이즈 측정, 2. 힙 영역에서 빈자리를 찾는 알고리즘 돌리기 3. 할당하기 과정을 거치기 때문입니다.
여기에 “힙 영역에서는 만약? 초기화가 안되가지고.. 이전 데이터의 쓰레기 값이 그대로 남아있다면” 프로그램 전체가 치명적인 버그에 빠질 수 있기 때문에,
JVM이 할당 과정에서 기본값(0, false, null)으로 안전하게 밀어버린게 아닐까 하는 개인적인 생각…
Resolution — 심볼릭 참조를 실제 참조로 연결
요약 : “이 이름이 실제로 JVM 안의 어느 클래스·필드·메서드를 가리키는가?”를 찾아 연결하는 단계!!
.class 파일의 Runtime Constant Pool에는 다른 클래스, 필드, 메서드가 메모리 주소처럼 직접 저장되어 있지 않습니다. 그대신 이름과 descriptor를 바탕으로 한 symbolic reference가 저장됨
java/lang/System.out
java/io/PrintStream.println:(Ljava/lang/String;)V
...
Resolution은 이런 심볼릭 참조를 JVM이 실제로 사용할 수 있는 직접 참조로 바꾸는 과정입니다.

Q. 왜 Symbolic reference를 사용할까요?
컴파일 시점에는 아직 프로그램이 실행되지 않았기 때문에, 클래스·필드·메서드가 JVM 내부의 어디에 준비될지 알 수 없습니다.
따라서 컴파일러는 실제 위치를 .class 파일에 기록하는 대신, 나중에 대상을 찾을 수 있도록 클래스 이름, 필드 이름, 메서드 이름과 descriptor를 symbolic reference 형태로 남겨 둡니다.
프로그램이 실행되고 관련 클래스가 로드되면 JVM은 이 이름 정보를 바탕으로 실제 클래스·필드·메서드를 찾고, 실행 중 사용할 수 있는 내부 참조로 연결합니다.
이 과정이 Resolution입니다.
여기서 말하는 직접 참조는 반드시 운영체제가 할당한 고정 메모리 주소를 의미하지는 않습니다.
JVM 구현에 따라 포인터, 핸들, 오프셋, 내부 인덱스처럼 실제 대상을 빠르게 찾아갈 수 있는 형태로 표현될 수 있습니다.
또한 모든 symbolic reference가 클래스 로딩 직후 한꺼번에 연결되는 것은 아닙니다. JVM은 해당 클래스·필드·메서드가 실제로 필요한 시점까지 Resolution을 미룰 수 있으며, 이를 흔히 lazy resolution이라고 합니다.
참고로,, C++이나 Swift처럼 컴파일과 링킹 과정을 거치는 언어도, 컴파일 단계에서는 아직 실제 위치가 확정되지 않은 함수나 변수의 이름을 심볼(Symbol) 로 기록하고 Symbol Table과 Relocation 정보를 생성합니다.
이후 링커(Linker) 가 여러 오브젝트 파일과 라이브러리의 심볼을 해석(Symbol Resolution)하여 실제 위치를 결정하고, Relocation을 수행해 참조를 수정합니다.
| C/C++, Swift | Java |
|---|---|
| Symbol Table | Runtime Constant Pool의 Symbolic Reference |
| Linker의 Symbol Resolution | JVM Resolution |
| Relocation | JVM 내부 직접 참조(Direct Reference)로 변환 |
| 실행 파일 생성 시 수행 | 클래스 로딩/실행 중 수행 |
3. Initialization — 개발자가 작성한 static 초기화 코드 실행
요약 : Initialization은 static 필드의 명시적 값과 static 블록을 실제로 실행하는 단계
Q. 일반 변수는 안하나요?
→ 네 .. 우리는 지금 Loading → Linking 에 이 지금은 Initialization인데, 이 과정은 전부 Method area(metaspace)에 class metadata를 로드하기 위해 준비하는 과정입니닷!
Linking이 끝난 뒤 “클래스가 실제로 처음 사용되어야 하는 상황이 되면” Initialization이 수행됩니다.
class Counter {
static int count = 100; // 명시적으로 초기화
static {
System.out.println("Counter initialized"); // 스태틱 블록
}
static int b = 20; // 명시적으로 초기화
}
컴파일러는 static 필드의 명시적 초기화와 static initializer block을 모아 특별한 클래스 초기화 메서드인 <clinit>으로 구성할 수 있습니다.
→ 개발자가 명시적으로 선언한 초기화 코드들이 클래스 내부에 여기저기 있을 수 있는데, 그걸 하나의 순서대로 모아서 하나의 메서드로 합쳐버린데요. 그게 바로 : static 전용 메서드
<clinit>은 개발자가 직접 선언하거나 호출하는 메서드가 아닙니다.
필요한 경우 컴파일러가 만들고, JVM이 클래스 초기화 시점에 실행합니다.
실제로 static 필드 초기화와 static block은 소스 코드에 작성된 순서대로 실행됩니다.
class Example {
static int first = initializeFirst();
static {
System.out.println("static block");
}
static int second = initializeSecond();
}
개념적인 실행 순서는 다음과 같습니다.
first 초기화
→ static block 실행
→ second 초기화
그리구 자식 클래스를 초기화해야 한다면, 아직 초기화되지 않은 부모 클래스가 먼저 초기화됩니다.
class Parent {
static {
System.out.println("Parent");
}
}
class Child extends Parent {
static {
System.out.println("Child");
}
}
new Child();를 호출하면 실행 결과는
Parent
Child
이렇게 됩니다.
핵심 정리
- JVM은 .class 파일을 발견하자마자 main()을 실행하지 않음!!!
- ClassLoader는 필요한 .class 파일의 바이트 데이터를 읽어 JVM에 전달
- Loading에서는 .class 파일을 JVM 내부의 런타임 타입으로 정의하고 Class Metadata를 구성함
- 클래스 로딩과 new를 통한 객체 생성은 서로 다른 과정
- JVM 명세에서는 Class Metadata를 Method Area에서 관리하며, HotSpot JVM에서는 이를 주로 Metaspace에 구현
- 대표적으로 Bootstrap, Platform, Application ClassLoader가 있으며 일반적으로 Parent Delegation 전략을 사용
- Linking은 Verification → Preparation → Resolution 세 단계로 구성됨
- Verification은 클래스 파일과 바이트코드가 JVM 규칙을 만족하는지 검사함
- Preparation에서는 static 필드를 위한 메모리를 준비하고 타입별 기본값을 설정함
- Resolution은 Runtime Constant Pool의 Symbolic Reference를 JVM 내부 참조로 연결함
- Initialization에서는 ()을 통해 static 필드 초기화와 static block이 작성된 순서대로 실행됨
- 부모 클래스는 자식 클래스보다 먼저 초기화됨
.class파일은 단순히 읽힌다고 실행 가능한 것이 아닙니다. JVM은 Loading, Linking, Initialization을 거쳐 해당 클래스를 안전하게 사용할 수 있는 런타임 타입으로 만듭니다.
다음 편
다음 편에서는 이어서 클래스 로드가 실제로 언제 로딩되거나 사라지는지 Active Use · ClassLoader · Unloading, 메모리 릭 관한 글 작성으로 이어가보려 합니다.
[Inside Java] 몰아보기
[Inside Java] 01. Java는 왜 바로 기계어로 컴파일하지 않을까? — 바이트코드부터 JVM 메모리까지...
Swift로 iOS 앱을 만들 때는 보통 컴파일 → 빌드 → 실행 파일 생성 → CPU 실행이라는 흐름이 비교적 자연스럽게 느껴졌습니다.C++도 비슷합니다. 소스 코드를 컴파일하면 특정 운영체제와 CPU가 이
dev-with-precious-dreams.tistory.com
[Inside Java] 02. 업캐스팅?! Java 컴파일러는 무엇을 알고, JVM은 무엇을 결정할까? — Dynamic Dispatch로
Parent p = new Child(); Java를 공부하다 보면 이 한 줄을 아주 자주 만나게 됩니다.보통은 이렇게 설명합니다.부모 타입 변수에 자식 객체를 담는 업캐스팅이다. 문법적으로는 맞는 설명입니다.그런데
dev-with-precious-dreams.tistory.com
[Inside Java] 03. Heap 객체에서 method Area까지! - Klass Pointer와 Dynamic vs Static Dispatch 살펴보기
이전 포스트(누르면 바로가져요) 에서는 다음 코드가 왜 문법적으로 가능한지 살펴봤습니다.Parent p = new Child(); 컴파일러는 표현식 p의 정적 타입을 Parent로 보고, 실제 생성되는 객체는 Child입니
dev-with-precious-dreams.tistory.com
[Inside Java] 05. JVM은 main()을 바로 실행하지 않는다 #2 — class metadata는 언제 초기화 될까? #Active Use,
앞선 4장에서는 .class 파일이 JVM 내부에서 Loading, Linking, Initialization을 거쳐 실행 가능한 타입이 되는 과정을 살펴봤습니다.하지만 여기서 한 가지 궁금증이 생깁니다. “클래스가 메모리에 로드
dev-with-precious-dreams.tistory.com
[Inside Java] 06. 같은 Java 코드가 실행될수록 빨라지는 이유 — JIT 컴파일과 HotSpot JVM 최적화 #Inline #
[Inside Java] 1~5편 까지는 JVM이 메모리를 어떻게 관리하는지를 중심으로 살펴보았습니다. 그런데 지금까지 살펴본 것처럼 JVM이 런타임마다 바이트코드를 하나씩 해석하고, Operand Stack을 이용해 연
dev-with-precious-dreams.tistory.com
[Inside Java] 07. interface는 JVM 메모리에서 어떻게 관리될까? — default 메서드와 enum 객체까지!!
이전 [Inside Java] 시리즈에서는 class로 만든 객체와 메서드 호출이 JVM 안에서 어떻게 연결되는지 살펴봤습니다.그런데 Java에는 class만 있는 것이 아니죠.....interface Animal { void speak();}enum Direction { LEF
dev-with-precious-dreams.tistory.com
[Inside Java] 08. 마무리.. Java 실행 구조 핵심 위키!
앞선 1장부터 7장까지 다룬 내용을 다시 찾기 쉽도록 핵심 개념만 두줄로 압축했습니다.자세한 동작 원리와 예시는 각 챕터에서 확인하고, 이 글에서는 개념의 연결 관계만 빠르게 정리합니다.
dev-with-precious-dreams.tistory.com
References
- Java Virtual Machine Specification, Chapter 5. Loading, Linking, and InitializingLoading, Linking(Verification, Preparation, Resolution), Initialization 과정의 공식 명세입
- https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-5.html
- Java Language Specification, Chapter 12. Execution클래스 초기화 시점, 부모 클래스 초기화, Active Use 등의 언어 명세를 설명
- https://docs.oracle.com/javase/specs/jls/se21/html/jls-12.html