본문 바로가기

Language/Java

[Java] JVM 클래스 로딩 Deep Dive 2편 — InstanceKlass, Class 객체, Initialization

1편에서는 .class 파일이 JVM에 로드되고 Linking을 거치는 과정을 살펴봤습니다. 이번 편에서는 그 결과물인 InstanceKlassClass 객체가 실제로 어떤 구조인지, 그리고 마지막 단계인 Initialization이 언제 어떻게 동작하는지를 다룹니다.

 


InstanceKlass — JVM이 클래스를 이해하는 방식

InstanceKlass의 위치와 역할

JVM은 .class 파일을 로드하고 나면 그 정보를 내부적으로 InstanceKlass라는 C++ 구조체로 표현합니다. JVM이 클래스를 실행하기 위해 필요한 모든 메타데이터를 담아두는 그릇입니다.

 

InstanceKlass는 Metaspace에 위치합니다. Metaspace는 Java 8에서 기존의 PermGen을 대체한 영역으로, JVM 프로세스의 네이티브 메모리를 사용합니다. GC 대상이 아니라 ClassLoader가 언로드될 때 함께 해제됩니다.

 

InstanceKlass가 담고 있는 것

  • 메서드 테이블 (vtable / itable) — 가상 메서드 디스패치를 위한 포인터 배열입니다. vtable은 클래스 상속, itable은 인터페이스 구현에 쓰입니다.
  • 상수 풀 (ConstantPool) — 클래스가 참조하는 문자열, 클래스명, 메서드 시그니처 등의 심볼릭 참조가 담깁니다. Resolution을 거치면 실제 포인터로 교체됩니다.
  • 필드 레이아웃 정보 — 인스턴스 필드의 오프셋, 타입 정보입니다.
  • 부모 클래스 / 인터페이스 참조 — 상속 계층 탐색에 사용됩니다.
  • _java_mirror — 힙에 있는 java.lang.Class 객체를 가리키는 포인터입니다. 이 연결이 Java 코드와 JVM 내부를 잇는 핵심입니다.

 


Class 객체 — Java 코드가 클래스를 바라보는 창

Class 객체의 위치와 역할

InstanceKlass가 JVM 내부의 표현이라면, java.lang.Class 객체는 Java 코드 레이어에서 클래스를 바라보는 창입니다. JVM 내부에서는 이를 java mirror(자바 미러) 라고 부릅니다.

 

Class 객체는 Java 힙에 위치합니다. Class.forName(), obj.getClass(), MyClass.class 등으로 얻는 객체가 바로 이것입니다.

 

Class 객체가 담고 있는 것

  • _klassInstanceKlass를 가리키는 숨겨진 포인터입니다. Java 코드에서는 보이지 않지만 JVM이 리플렉션 등을 처리할 때 이 포인터를 타고 InstanceKlass로 접근합니다.
  • classLoader — 이 클래스를 로드한 ClassLoader 참조입니다.
  • reflectionDataSoftReference로 감싸진 리플렉션 캐시입니다. fields[], methods[], constructors[]가 여기 들어갑니다. 메모리 부족 시 GC 대상이 되고, 이후 리플렉션 호출 시 다시 생성됩니다.
  • static 필드 값 (Java 8+) — Java 7까지는 static 필드 값이 PermGen에 저장됐지만, Java 8부터는 Class 객체 안에 저장됩니다. 힙에 있으므로 GC가 관리할 수 있습니다.

Class 객체는 해당 ClassLoader가 GC될 때 함께 수거됩니다. ClassLoader가 살아있는 한 Class 객체도 힙에 유지되고, ClassLoader가 언로드되면 Class 객체(힙)와 InstanceKlass(Metaspace) 모두 해제됩니다.

 

InstanceKlass ↔ Class 객체 — 양방향 연결

두 구조체는 양방향 포인터로 연결됩니다.

  • InstanceKlass._java_mirror → Class 객체
  • Class 객체._klass → InstanceKlass

 

이 연결은 Loading 단계 마지막에 java_lang_Class::create_mirror()가 호출되면서 만들어집니다. 리플렉션, instanceof 검사, 가상 메서드 디스패치 등 Java의 핵심 기능들이 모두 이 양방향 포인터를 타고 동작합니다.

 


Initialization — static이 실제로 살아나는 순간

Initialization이란

Linking까지 마친 클래스는 아직 완전히 실행 가능한 상태가 아닙니다. Preparation 단계에서 static 필드는 기본값(0, null 등)만 채워진 상태입니다. Initialization은 개발자가 작성한 static 초기값과 static 블록을 실행해서 클래스를 완전히 초기화하는 단계입니다.

 

JVM은 이 과정을 <clinit>이라는 특수 메서드로 처리합니다. javac가 컴파일 시 static 필드 초기화와 static 블록을 하나로 합쳐서 자동으로 생성하는 메서드입니다. static 필드도 static 블록도 없는 클래스라면 <clinit>은 생성되지 않습니다.

public class Counter {
    static int count = 100;          // ①

    static {
        System.out.println("초기화"); // ②
        count = 200;
    }
}

위 코드에서 <clinit>은 ① → ② 순서로 실행됩니다. static 필드 초기화와 static 블록은 소스 코드에 등장하는 순서대로 실행됩니다.

 

Initialization 트리거

Initialization은 클래스가 처음으로 능동적으로 사용될 때 딱 한 번 실행됩니다. JVM 명세는 다음 상황을 트리거로 정의합니다.

  • new MyClass() — 인스턴스를 생성할 때
  • MyClass.FIELD — static 필드에 접근하거나 값을 쓸 때. 단, static final 상수는 컴파일 타임에 인라인되므로 트리거가 되지 않습니다.
  • MyClass.method() — static 메서드를 호출할 때
  • Class.forName("MyClass") — 리플렉션으로 클래스를 로드할 때 (initialize=true인 경우)
  • 서브클래스가 초기화될 때 — 부모 클래스가 아직 초기화되지 않았다면 부모 먼저 초기화됩니다.

 

JVM이 보장하는 스레드 안전성

JVM은 <clinit> 실행을 스레드 안전하게 단 한 번만 보장합니다. 멀티스레드 환경에서 두 스레드가 동시에 같은 클래스를 처음 사용하려 해도, 한 스레드만 <clinit>을 실행하고 나머지는 완료될 때까지 대기합니다.

 

이 특성을 활용한 것이 Initialization-on-demand holder 패턴입니다.

public class Singleton {
    private Singleton() {}

    private static class Holder {
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE; // Holder 클래스 최초 접근 시 <clinit> 실행
    }
}

Holder 클래스는 getInstance()가 처음 호출되는 시점에 초기화되고, JVM이 <clinit>의 단일 실행을 보장하므로 별도의 synchronized 없이도 안전한 싱글톤이 만들어집니다.

 


정리

[InstanceKlass]  Metaspace
  - vtable / itable  : 메서드 디스패치
  - ConstantPool     : 심볼릭 참조 (Resolution 후 실제 포인터로 교체)
  - 필드 레이아웃     : 인스턴스 필드 오프셋
  - _java_mirror     : Class 객체와 연결

[Class 객체]  Java Heap
  - _klass           : InstanceKlass와 연결
  - classLoader      : 로드한 ClassLoader
  - reflectionData   : 리플렉션 캐시 (SoftReference)
  - static 필드 값   : Java 8+부터 여기 저장

[Initialization]
  - 트리거: new, static 접근/호출, Class.forName(), 서브클래스 초기화
  - <clinit> 실행: static 필드 실제 값 대입 + static 블록 (선언 순서대로)
  - static 필드도 static 블록도 없으면 <clinit> 생성 안 됨
  - JVM이 스레드 안전하게 단 한 번만 보장

 

클래스 로딩의 전 과정이 Loading → Linking → Initialization으로 이어지고, 그 결과 InstanceKlass와 Class 객체가 양방향으로 연결된 완전한 상태가 만들어집니다. 3편에서는 이 과정의 주체인 ClassLoader를 더 깊이 살펴봅니다.