
앞선 4장에서는 .class 파일이 JVM 내부에서 Loading, Linking, Initialization을 거쳐 실행 가능한 타입이 되는 과정을 살펴봤습니다.
하지만 여기서 한 가지 궁금증이 생깁니다.
“클래스가 메모리에 로드되었다면, 항상 그 즉시 초기화되는 것일까요?”
정답은 아닙니다.
JVM은 클래스가 실제로 필요한 순간에만 초기화를 수행합니다.
이번 글에서는 JVM 명세에서 말하는 Active Use를 시작으로, 클래스의 정체성을 결정하는 ClassLoader 그리고 클래스가 메모리에서 사라지는 과정까지 살펴보겠습니다.
메서드 영역에 클래스가 로딩·초기화되는 시점은 언제일까? (Active Use)
클래스가 메서드 영역에 로드되었다고 반드시 즉시 초기화되는 것은 아닙니다.
이전 포스트에서 한번 언급했지만, JVM은 해당 클래스를 실제로 적극적으로 사용하는 시점에 초기화를 수행합니다.
대표적인 경우는 다음과 같습니다.
new Counter(); // 인스턴스 생성
Counter.count++; // static 필드 사용
Counter.run(); // static 메서드 호출
Class.forName("Counter"); // 기본 설정에서는 초기화까지 수행
또한 실행 시작 클래스의 main()을 호출하기 전에도 해당 클래스가 초기화되어야 합니다.
그런데 모든 클래스 참조가 초기화를 발생시키는 것은 아닙니다.
컴파일 타임 상수 사용
class Constants {
static final int LIMIT = 100;
static {
System.out.println("Constants initialized");
}
}
...
// main() scope안에서의 호출
System.out.println(Constants.LIMIT);
LIMIT이 컴파일 타임 상수라면 값이 사용하는 클래스의 Constant Pool에 포함될 수 있습니다.
이 경우 Constants클래스가 메서드 영역에 loading → linking → initialization 과정 없이 값을 사용할 수 있습니다.
이유는 위에서 설명한 그 바로 ConstantValue니까요!
배열 타입 생성
Counter[] counters = new Counter[10];
이 코드는 Counter 인스턴스를 만드는 것이 아니라 Counter[] 배열 객체를 만듭니다.
따라서 이것만으로 Counter 클래스의 Initialization이 반드시 발생하는 것은 아닙니다.
그러나 배열을 만들기 위해서, Counter타입의 클래스 존재, 정보를 알아야 해서 Loading, Linking은 일어날 수 있습니다.
근데 클래스가 적극적으로 사용된다는 코드는 아니어서 Initialization은 발생 안합니다.
이처럼 Loading과 Initialization은 같은 시점이 아닐 수 있습니다.
Q. 같은 이름의 클래스는 항상 같은 타입일까요?
Java에서 클래스의 정체성은 단순히 패키지명과 클래스명만으로 결정되지 않습니다.
개념적으로 다음 두 요소가 함께 타입을 구분합니다.
클래스의 전체 이름 + 그 클래스를 정의한 ClassLoader
따라서 동일한 com.example.User 바이트코드를 서로 다른 ClassLoader가 각각 정의하면 JVM에서는 서로 다른 타입으로 취급될 수 있습니다.

이 개념은 애플리케이션 서버, 플러그인 구조, 개발 도구의 재시작 기능처럼 서로 다른 클래스 영역을 분리해야 하는 환경에서 중요합니다.
그런데 이번 글에서는 구조를 이해하는 정도로만 기억하면 좋다는 생각이 듭니다.
클래스 이름이 같더라도 정의한 ClassLoader가 다르면 같은 타입이 아닐 수 있다.
클래스의 사용과 Unloading
클래스는 한 번 (메서드 영역-Metaspace에) 로드되면 일반 객체처럼 쉽게 제거되지 않습니다.
일반적인 애플리케이션에서는 Application ClassLoader가 JVM이 종료될 때까지 계속 살아 있기 때문에, 그 ClassLoader가 정의한 클래스도 대부분 프로그램 종료까지 유지됩니다.
클래스가 Unloading 대상이 되려면 해당 클래스를 정의한 ClassLoader 자체가 더 이상 도달 가능하지 않아야 하고 그 ClassLoader가 정의한 클래스와 관련 객체들도 더 이상 사용되지 않아야 합니다.

그래서 클래스 Unloading은 일반 객체 하나가 GC되는 문제라기보다 ClassLoader의 생명주기와 연결된 문제에 가깝습니다.
일반적인 작은 Java 프로그램에서는 이 부분을 깊게 신경 쓸 일이 많지 않습니다. 하지만 애플리케이션 서버의 재배포, 플러그인 시스템, 개발 도구의 hot reload 환경에서는 ClassLoader가 계속 남아 클래스가 해제되지 않는 문제가 메모리 누수로 이어질 수 있습니다.
클래스(Class Metadata)는 클래스 객체 인스턴스(Object)와 다릅니다.
클래스는 JVM이 직접 관리하는 것이 아니라 "어떤 ClassLoader가 로드한 클래스인가" 와 항상 연결되어 있습니다.
Application ClassLoader
│
├── Main.class
├── Member.class
└── User.class
클래스 메타데이터는 내부적으로 이렇게 모두 ClassLoader가 소유한다고 생각하면 됩니다.
일반적인 Java 프로그램에서는?
JVM
└── ApplicationClassLoader
├── Main
├── Member
└── User
ApplicationClassLoader는 JVM이 종료될 때까지 살아 있습니다.
ApplicationClassLoader
↓
Main.class
그래서 이 참조가 절대 끊어지지 않습니다. Main.class, Member.class, User.class 모두 Unloading되지 않습니다.
그렇지만 프로그램 종료할 때 JVM이 통째로 내려갑니다.
그래서 일반적인 Java 프로그램에서는 Class Unloading을 거의 볼 일이 없습니다.
그럼 언제 ClassLoader가 사라질까?
대표적인 예가 웹 서버(Tomcat) 입니다.
Tomcat에서는 웹 애플리케이션마다 ClassLoader를 하나씩 만듭니다.
JVM
│
├── CommonClassLoader
│
├── WebAppClassLoader (A)
│ ├── Controller
│ ├── Service
│ └── Repository
│
└── WebAppClassLoader (B)
├── Controller
└── Service
A 웹사이트와 B 웹사이트는 서로 다른 ClassLoader를 사용합니다.
Q. A 웹사이트를 삭제하면..?
그럼 Tomcat은 WebAppClassLoader(A)를 더 이상 참조하지 않습니다. 그렇게 되면 클래스 로더, 클래스 메타데이터, 스태틱변수, runtime constant pool 등 전부 GC로부터 제거대상!
그런데 메모리 누수가 생기는 경우도 있습니다.
문제는 웹 애플리케이션보다 더 오래 살아 있는 객체가 WebAppClassLoader가 로드한 객체를 계속 참조할 때 발생합니다.
예를 들어 GlobalCache가 Tomcat의 공용 ClassLoader처럼 웹 애플리케이션보다 오래 살아 있는 ClassLoader에 의해 로드되었다고 가정하겠습니다.
public class GlobalCache { public static Object obj; }
...
//그리고 추가적으로
GlobalCache.obj = new MyService();
이렇게 저장했는데 저장했는데 MyService는 WebAppClassLoader(A)가 만든 클래스 메타데이터 입니다.

웹 애플리케이션 A가 종료되거나 재배포되면 Tomcat은 원래 사용하던 WebAppClassLoader(A) 참조를 제거합니다.
하지만 GlobalCache가 여전히 MyService 객체를 보관하고 있다면, 다른 참조 경로를 통해 WebAppClassLoader(A)까지 계속 도달할 수 있습니다.
따라서 해당 ClassLoader와 그 ClassLoader가 로드한 클래스 메타데이터가 Unloading되지 않습니다.
이것이 ClassLoader Memory Leak입니다.
예시가,,,
ThreadLocal- Thread Pool
- JDBC Driver
- static Singleton
- 공용 라이브러리의 static 캐시
등 웹 애플리케이션보다 오래 살아 있는 객체가 웹앱의 객체를 참조하는 경우 발생 가능합니다.
retain cycle에 의한 메모리 릭이 아니어도 이렇게 발생할 수 있네요… ㄴㅇㄱ
핵심 정리
- 클래스가 로딩되었다고 반드시 즉시 초기화되는 것은 아닙니다.
- JVM은 클래스를 실제로 적극적으로 사용하는 시점(Active Use)에 Initialization을 수행합니다.
- new, static 필드 접근, static 메서드 호출, Class.forName() 등은 대표적인 Active Use입니다.
- 컴파일 타임 상수 사용이나 배열 타입 생성은 일반적으로 클래스 Initialization을 발생시키지 않습니다.
- Java에서 클래스의 정체성은 클래스 이름과 ClassLoader의 조합으로 결정됩니다.
- 같은 이름의 클래스라도 서로 다른 ClassLoader가 정의하면 서로 다른 타입입니다.
- 일반적인 Java 프로그램에서는 Application ClassLoader가 JVM 종료까지 살아 있으므로 클래스도 대부분 Unloading되지 않습니다.
- Class Unloading은 클래스보다 해당 클래스를 정의한 ClassLoader의 생명주기와 연결됩니다.
- Tomcat은 웹 애플리케이션마다 별도의 ClassLoader를 사용하여 애플리케이션을 격리합니다.
- 웹 애플리케이션보다 오래 살아 있는 객체가 웹 애플리케이션의 객체를 참조하면 ClassLoader가 GC되지 못하고 ClassLoader Memory Leak이 발생할 수 있습니다.
클래스의 생명주기는 Loading에서 끝나지 않습니다.
JVM은 Active Use에 따라 클래스를 초기화하고, ClassLoader의 생명주기에 따라 클래스를 유지하거나 해제합니다.
다음편에서는 JVM은 어떻게 점점 빨라질까에 대한 글을 쓰려합니다. (Interpreter · JIT · Code Cache · Inlining)
[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] 04. JVM은 main()을 바로 실행하지 않는다?! #1 -ClassLoader, 클래스 생명주기 #Loading #Linking #In
1편에서는 Java 코드가 바이트코드로 변환되고 JVM 위에서 실행되는 전체 흐름을 살펴봤습니다. 2편에서는 컴파일러가 정적 타입을 기준으로 무엇을 검사하고, 런타임의 JVM이 실제 객체를 기준으
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 Language Specification, Chapter 12. Execution
https://docs.oracle.com/javase/specs/jls/se21/html/jls-12.html - Active Use, 클래스 초기화 조건, 컴파일 타임 상수 사용 등을 설명하는 공식 명세
- Java Virtual Machine Specification, Chapter 5. Loading, Linking, and Initializing
https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-5.html - 클래스 로딩과 초기화, Unloading의 기본 동작을 정의한 JVM 명세
- Eclipse Memory Analyzer (MAT)
https://eclipse.dev/mat/ - ClassLoader Memory Leak 분석 시 가장 많이 사용하는 메모리 분석 도구!!