자바는 "한 번 작성하면 어디서나 실행된다"는 철학으로 유명합니다. 이 철학의 중심에는 JVM 클래스 로딩 시스템이 있습니다.
이번 글에서는 JVM이 .class 파일을 어떻게 메모리에 올리고 실행하는지, 그 핵심 매커니즘을 상세히 설명합니다.
1. 클래스 로딩이란 무엇인가?
자바 프로그램은 .java 소스 파일을 컴파일하면 .class 파일(바이트코드)이 생성됩니다.
JVM은 이 .class 파일을 메모리에 로드하고 실행하는데, 이 과정을 클래스 로딩(Class Loading)이라고 부릅니다.
굳이 java파일을 class 파일로 변환하는 이유는, JVM이 이를 읽을 수 없어서가 아니라 효율성, 이식성, 보안성을 위해서 JVM이 읽기 쉬운 파일로 변환한다고 보시면 됩니다.
자바 프로그램의 실행 흐름 개요
자바 프로그램이 실행될 때, 다음과 같은 단계를 거칩니다.
- 프로세스 시작
- JVM 메모리 초기화 및 GC 초기화
- 필수 클래스 로드
- static main() 메서드 실행
- 필요할 때마다 다른 클래스 로드 (지연 로딩)
이 과정에서 3, 5번 두번에 걸쳐서 .class 파일을 메모리에 로드합니다. 이 과정은 클래스 로더(ClassLoader)가 담당합니다.
클래스 로더가 필요한 이유
- 자바는 클래스 단위로 프로그램을 구성함
- 클래스 수백 개가 동적으로 로딩될 수 있음
- 로딩 시점, 범위, 책임 등을 체계적으로 관리할 필요 있음
클래스 로더는 이러한 동적 클래스 로딩의 정책과 생명주기를 관리하기에 필요합니다.
2. 클래스 로더의 계층 구조
JVM은 클래스를 로드할 때 하나의 처리로 하지 않고, 여러 계층의 로더를 통해 관리합니다.
Bootstrap ClassLoader
Object 같은 기초적인 클래스들을 가져오는 클래스로더입니다.
다른 클래스 로더와는 다르게 c 또는 c++로 만들어진 클래스 로더로 JVM 런타임에서는 참조가 되지 않습니다.
ClassLoader classLoader = Object.class.getClassLoader();
System.out.println("classLoader = " + classLoader);
// 결과 : classLoader = null
// c, c++ 의 값을 JVM이 가져올 수 없기 때문에 null 로 없는것 처럼 보입니다.
JVM 바이너리 실행(프로세스 시작) -> 런타임 초기화(메모리 구조, GC 초기화) -> BootStrap ClassLoader 실행 순서입니다.
Platform ClassLoader
BootStrap ClassLoader단계가 끝나면 다음 단계로 Platform ClassLoader가 동작합니다.
Platform ClassLoader는 JDK 내부 API와 JDK 모듈을 로딩합니다.
ClassLoader classLoader = HttpClient.class.getClassLoader();
System.out.println("classLoader = " + classLoader);
// 결과 : classLoader = jdk.internal.loader.ClassLoaders$PlatformClassLoader@4a574795
과거 Java8 이하에서는 특정 디렉토리에 jar를 넣어, 확장 목적으로 사용하는 Extension ClassLoader라고 불렀습니다.
그러나 버전 충돌 문제, 디렉토리 내에 악성 파일을 넣는 공격으로 인해 Java 9 모듈 시스템 도입과 함께 클래스 로더 체계가 개편되었습니다.
Application ClassLoader
jvm을 실행할 때 필수적으로 실행되는 클래스 로더의 마지막 ClassLoader입니다.
우리가 작성한 일반 클래스들 (classpath에 있는 파일들)을 로딩합니다.
public class MyClass {
public static void main(String[] args) {
ClassLoader classLoader = MyClass.class.getClassLoader();
System.out.println("classLoader = " + classLoader);
}
}
// 결과 : classLoader = jdk.internal.loader.ClassLoaders$AppClassLoader@18b4aac2
사용자 정의 ClassLoader
Spring 같은 프레임워크나 애플리케이션 서버에서 사용하는 클래스 로드들도 존재합니다.
외부 특정 경로(BOOT-INF/classes/ 등) 에 접근할 수 있도록 하거나, 동적으로 .class 데이터를 읽어들이는 것도 가능합니다.
(필요시 https 응답을 통해 클래스를 생성하거나, 런타임 시 직접 .class를 만들고 로딩할 수 있습니다. 예: CGLIB, Bytebuddy)
Delegation Model (위임 모델)
JVM 클래스 로더는 "부모에게 먼저 위임"하는 구조입니다. 위에서 클래스 로더는 계층형 구조를 가지고 있다고 했습니다.
부모 클래스 로더가 해당 클래스를 로드할 수 없다면, 그 때 자신이 시도한다.
그렇기에 위 방식을 채택하게 되고, 이 방식은 중복 로딩 방지, 보안 확보, 일관성 유지라는 장점이 있습니다.
계층은 다음과 같이 확인할 수 있습니다.
ClassLoader classLoader = MyClass.class.getClassLoader();
System.out.println("classLoader = " + classLoader);
ClassLoader parentClassLoader = classLoader.getParent();
System.out.println("parentClassLoader = " + parentClassLoader);
ClassLoader grandParentClassLoader = parentClassLoader.getParent();
System.out.println("grandParentClassLoader = " + grandParentClassLoader);
// 결과
// classLoader = jdk.internal.loader.ClassLoaders$AppClassLoader@18b4aac2
// parentClassLoader = jdk.internal.loader.ClassLoaders$PlatformClassLoader@30f39991
// grandParentClassLoader = null <- c/c++ 이기에 null로 표시됨. BootStrapClassLoader를 뜻함
3. 클래스 로딩의 전체 과정
클래스 로딩은 단순히 메모리에 올리는 것 이상입니다. JVM은 다음의 3단계를 거쳐 클래스가 실행 가능한 상태로 만듭니다.
1단계. Loading
.class 파일을 클래스 로더가 읽어들여 메모리의 메타영역(Metaspace)에 로드합니다.
Class 객체가 생성되며, 내부는 초기화 되지 않은 상태입니다.
2단계. Linking
링크 과정은 클래스의 바이트코드를 JVM 내부에서 사용 가능한 구조로 준비하는 과정입니다.
- Verification : 바이트코드를 분석하고 검증합니다. 명령어가 실행될 수 있는지 검사합니다.
- Preparation : static field에 기본값을 할당합니다. static block의 실행은 수행하지 않습니다.
static int a = 3; 이라는 필드가 있을 때 기본값은 3이 아닌 0을 말하는 것으로 사용가능하도록 "진짜" 필드 초기화를 말합니다. - (Optional) Resolution : 심볼릭 레퍼런스를 실제 메모리 주소로 변경합니다. (런타임에 수행될 수 있습니다.)
3단계. Initialization
생성된 객체의 내부를 초기화 하는 과정을 말합니다.
- static 블록과 static 필드에 명시된 값들을 실제로 초기화합니다.
- 클래스가 최초로 사용되는 시점에 초기화됩니다.
static int x = 10;
static {
x = 2;
}
이후 인스턴스 생성은 초기화가 끝난 클래스 객체를 이용하여 만들어집니다.
정리: 클래스 로딩 전체 흐름
[.class 파일]
↓
[ClassLoader] → Loading
↓
Linking → Verification → Preparation → (Resolution)
↓
Initialization → static {} 실행
↓
[Class 객체 사용 가능]
마무리하며
자바의 클래스 로딩 매커니즘은 단순한 "파일 로딩"을 넘어, 보안성, 유연성, 동적 모듈화까지 가능하게 해주는 강력한 기능입니다.
저는 클래스 로딩이라는 것이 Java 개발에 있어서 중요하다고 생각하며,
이 과정을 이해하고 아래의 것들을 학습해보시길 바랍니다.
- main 메서드가 왜 static 메서드로 작성이 될 수밖에 없는지
-> static 메서드를 사용하지 않는다고 하면, 객체를 생성해야 하지만 객체를 생성하기 위해서는 static 블럭이나 메서드가 필요하므로, 개념이 순환함 - 리플랙션이라는 기술이 어떤 객체를 사용하는지
-> 클래스 로딩 시점에 생성된 Class 객체를 활용함, Class 객체는 여러 인스턴스 메서드를 제공함 - 클래스 객체에 대한 모니터락 사용 시 어떤 부작용이 발생할 수 있는지
-> 클래스 객체는 .class 당 1개만 생성되므로 모니터 락 또한 싱글톤의 형태를 띔 - 프레임워크의 동적 클래스 생성 원리 (Spring, Hibernate 의 CGLIB, Bytebuddy 등)
-> 런타임 시점에 .class 데이터(파일과 크게 다르지 않음) 를 만들고 이를 로딩하여 마치 직접 코드를 작성한 것 처럼 동작함 - JDK에서 발생하는 ClassNotFoundException, NoClassDefFoundError의 본질
-> Class.forName("java.lang.UserService"); 로 .class 파일을 못찾았거나, 없어진 경우에 발생
'Java & Kotlin' 카테고리의 다른 글
NIO(New I/O)와 Webflux 비동기 프로그래밍 (1) | 2025.07.07 |
---|---|
Java Stream API의 핵심 개념과 병렬 처리의 함정 (0) | 2025.06.15 |
Socket 통신 (1) | 2024.12.15 |
Java ThreadPoolExecutor 예외 처리와 Exception Handler (0) | 2024.10.09 |
Java Thread로 직접 구현하는 커스텀 쓰레드 풀: 기본 원리부터 동작까지 (0) | 2024.10.06 |