본문 바로가기

클래스 로더가 로딩 + 링크 + 초기화를 모두 한다는 오해

@정소민fan2026. 5. 31. 17:16

요새 JVM을 공부하는 중인데... 레퍼런스를 계속 찾아다보다 보니 클래스 로더가 로딩, 링크, 초기화를 모두 수행한다는 레퍼런스들이 많았다. openjdk 25 소스코드를 직접 풀 받아서 분석하는 중인데, 클래스 로더가 모두 맡는다는 것은 오해다.

정확히 말하면 클래스 로더는 로딩과 링크 일부만 맡아서 하고, 초기화는 실행 엔진이 수행한다.

왜 이런 결론이 나왔는지, 직접 소스코드를 보면서 확인해보겠다.

 

초기화는 언제 발생하는가?

로딩의 경우에는 JVM 명세에 정확히 명시되어있지 않다. JVM을 구현하는 곳마다 자유롭게 구현할수 있다는 뜻이다.

단, 초기화는 정확히 명시되어 있다.

  1. new, getstatic, putstatic, invokestatic 명령어를 만났을 때, 타입이 초기화되지 않았다면 초기화를 촉발함
  2. 리플렉션 메서드를 사용할 때
  3. 클래스를 초기화할 때 상위 클래스가 초기화되지 않았다면
  4. 메인 타입 메서드를 포함하는 클래스나 인터페이스를 찾아 초기화 실행.
  5. MethodHandle 인스턴스를 호출할 때 해당하는 클래스가 초기화되지 않았다면 초기화
  6. 디폴트 메서드를 정의한 인터페이스를 구현한 클래스가 초기화될때 인터페이스부터 초기화

다 찾아보기에는 너무 길고 많아서, new 바이트코드를 만났을때만 한번 보자.

// bytecodeInterpreter.cpp : 1985	
    CASE(_new): {
        u2 index = Bytes::get_Java_u2(pc+1);
        
        ConstantPool* constants = istate->method()->constants();
        if (UseTLAB && !constants->tag_at(index).is_unresolved_klass()) {
        // 이미 해석된 클래스일 경우, 클래스 로딩 시작 X
          ...
        }
        // Slow case allocation
        CALL_VM(InterpreterRuntime::_new(THREAD, METHOD->constants(), index),
                handle_exception);
        OrderAccess::storestore();
        SET_STACK_OBJECT(THREAD->vm_result_oop(), 0);
        THREAD->set_vm_result_oop(nullptr);
        UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1);
      }

실제 소스코드의 `bytecodeInterpreter.cpp` 파일이다. 인터프리터가 바이트코드를 하나씩 읽다가 new 키워드를 만나게 되면, 상수 풀을 확인하여 이미 직접 참조로 해석되었는지 확인한다.

아직 심볼 참조라면, 해석을 위해 `InterpreterRuntime::_new`를 호출한다.

참고로 해석이 무엇이냐면, 인스턴스를 만들기 위한 틀(메타데이터)가 필요한데 해당 틀의 주소가 어디있는지 아직 모르고, 틀의 "이름"만 알고있기 때문에 틀의 "이름"을 갖고 주소를 찾는 과정이다. 자세한건 나중에 포스팅해보겠다.

 

`u2 index`는 바이트코드에서 new 키워드 다음에 나오는 상수 풀 번호를 의미한다. 이 번호는 해석되면 메타스페이스의 주소로 변환된다.

이미 해석된 상수 풀 엔트리인지 확인하는 코드도 보일 것이다. 

`InterpreterRuntime::_new`를 호출할 때 이 index를 넘겨주는 것도 보일것이다. `!constants->tag_at(index).is_unresolved_klass()` 이 부분에서!

 

그리고 더 참고로... 이 바이트코드 인터프리터 파일은 최신 jdk에서는 안쓰인다고 한다. 너무 느려서 그렇다나 뭐라나... 제미나이가 그러기는 하는데 요새 이놈이 영 신통치 않아서 믿을수가 없다.

 

그랬는데...? 요새 "JVM 밑바닥부터 파헤치기"를 보고 공부하고 있었는데, 2.3장에서 바로 위와 똑같은 코드와 말이 나왔다.

 

// interpreterRuntime.cpp : 214
JRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* current, ConstantPool* pool, int index))
  Klass* k = pool->klass_at(index, CHECK);
  InstanceKlass* klass = InstanceKlass::cast(k);

  // Make sure we are not instantiating an abstract klass
  klass->check_valid_for_instantiation(true, CHECK);

  // Make sure klass is initialized
  klass->initialize(CHECK); // InstanceKlass::initialize

  oop obj = klass->allocate_instance(CHECK);
  current->set_vm_result_oop(obj);
JRT_END

이 코드로 넘어오는데, 딱 보니까 initialize라는 함수를 통해 초기화를 하는 것 같다. 그런데 이 코드는 인터프리터가 호출하는 코드다.

따라서 클래스 로더가 모든 과정을 담당한다는 말 자체는 벌써 틀렸다는 것을 알 수 있다.

 

그러면 클래스 로더가 하는 일은 어디에 있는가??

이 부분은 `Klass* k = pool->klass_at(index, CHECK);` 으로 들어가봐야 한다. 이쪽 부분은 너무 깊으니까... 딱히 보지 않아도 된다.

클래스 로더가 일을 하는 부분

`klass_at` 부분으로 넘어가보자.

// constantPool.hpp : 384  
  
  Klass* klass_at(int cp_index, TRAPS) {
    constantPoolHandle h_this(THREAD, this);
    return klass_at_impl(h_this, cp_index, THREAD);
  }

인터프리터가 바이트코드를 실행시키던 스레드에서 상수 풀 핸들러를 `h_this`로 가져온다.

그리고 다시 `klass_at_impl` 함수를 호출한다. 참고로 이렇게 impl 함수를 다시 호출하는 코드가 굉장히 많다.

// constantPool.cpp : 622

Klass* ConstantPool::klass_at_impl(
const constantPoolHandle& this_cp, 
int cp_index,TRAPS) 
{
...
	// The tag must be JVM_CONSTANT_Class in order to read the correct value from
  // the unresolved_klasses() array.
  if (this_cp->tag_at(cp_index).is_klass()) { // 이미 해석이 완료되었다면
    Klass* klass = this_cp->resolved_klasses()->at(resolved_klass_index);
    assert(klass != nullptr, "must be resolved");
    return klass;
  }
  
...
//constantPool.cpp : 656

  HandleMark hm(THREAD);
  Handle mirror_handle;
  Symbol* name = this_cp->symbol_at(name_index);
  Handle loader (THREAD, this_cp->pool_holder()->class_loader());

  Klass* k;
  {
    // Turn off the single stepping while doing class resolution
    JvmtiHideSingleStepping jhss(javaThread);
    k = SystemDictionary::resolve_or_fail(name, loader, true, THREAD);
  } //  JvmtiHideSingleStepping jhss(javaThread);
  ...

`if (this_cp->tag_at(cp_index).is_klass())` 코드로 이미 해석되었는지 또 확인한다.

아마 다른 바이트코드 분기에서도 많이 쓰이고, 스레드의 경쟁으로 인해 이미 해석되어버릴수도 있어서 그런거 아닐까?

 

그리고 밑으로 더 내려오면, 상수풀로부터 인덱스를 통해 심볼(이름)을 가져오고, 이를 `resolve_or_fail` 함수를 호출한다. 아마 이름 상 해석을 위한 코드가 아닐까?

// systemDictionary.cpp : 334

Klass* SystemDictionary::resolve_or_fail(Symbol* class_name, Handle class_loader,
                                         bool throw_error, TRAPS) {
  Klass* klass = resolve_or_null(class_name, class_loader, THREAD);
  // Check for pending exception or null klass, and throw exception
  if (HAS_PENDING_EXCEPTION || klass == nullptr) {
    handle_resolution_exception(class_name, throw_error, CHECK_NULL);
  }
  return klass;
}

여기서는 또 `resolve_or_null`이 호출된다. 그리고 호출 코드 밑에서는 에러 플래그가 세워졌는지 (`HAS_PENDING_EXCEPTION`) 확인하고 맞다면 예외를 발생시킨다. 그런데 왜 진작 예외를 발생시키지 않고 플래그만 세우고 바깥에서 예외를 던지는 걸까?? 제미나이는 성능을 위한 jdk 개발자들의 치열한 고민 흔적이라는데... 맞는지 모르겠다.

 

// systemDictionary.cpp : 346

// Forwards to resolve_array_class_or_null or resolve_instance_class_or_null

Klass* SystemDictionary::resolve_or_null(Symbol* class_name, Handle class_loader, TRAPS) {
  if (Signature::is_array(class_name)) {
    return resolve_array_class_or_null(class_name, class_loader, THREAD);
  } else {
    assert(class_name != nullptr && !Signature::is_array(class_name), "must be");
    if (Signature::has_envelope(class_name)) {
      ResourceMark rm(THREAD);
      // Ignore wrapping L and ;.
      TempNewSymbol name = SymbolTable::new_symbol(class_name->as_C_string() + 1,
                                                   class_name->utf8_length() - 2);
      return resolve_instance_class_or_null(name, class_loader, THREAD);
    } else {
      return resolve_instance_class_or_null(class_name, class_loader, THREAD);
    }
  }
}

`resolve_or_null`에서는 해당 심볼의 시그니처가 배열인지 확인한다. 일반 클래스와 클래스 배열의 로딩이 서로 다르기 때문에 분리해놓은듯 하다. 이 부분은 공부가 좀더 필요하다.

그리고 주석 중에 래핑된 L과 ; 를 뺀다는 부분이 있다. 자바 세상에서 다루는 클래스 네임과 JVM에서 다루는 이름은 차이가 있다. 이걸 시그니처 이름이라고 하던가??

예를 들면 `java.lang.String`은 `Ljava/lang/String;`으로 바뀐다. 맨 앞의 L은 객체 참조라는 뜻이다. `has_evelope`이 true라는건 이름이 객체 참조형이고, ;이 붙어있다는 뜻이라 미리 떼어주고 resolve_instance_class_or_null을 호출하는 것이다.

 

// systemDictionary.cpp : 576

InstanceKlass* SystemDictionary::resolve_instance_class_or_null(Symbol* name,
                                                                Handle class_loader,
                                                                TRAPS) {
...
  // systemDictionary.cpp : 591

  // Do lookup to see if class already exists.
  InstanceKlass* probe = dictionary->find_class(THREAD, name);
  if (probe != nullptr) return probe;

그러면 `resolve_instance_class_or_null`함수로 들어가보자.

여기서는 `dictionary->find_class`를 사용하고 있는데 이는 시스템 딕셔너리라는 곳에서 이 클래스를 찾는 코드다.

시스템 딕셔너리는 메타스페이스 영역에 로딩된 Klass 인스턴스의 참조를 저장해두는 Map 과 유사한 자료구조이다.

상수 풀은 각 클래스마다 하나씩 있지만, 상수 풀에서 해석하고 싶어하는 클래스들은 메타스페이스에 이미 로딩되어있을 수 있다. 따라서 미리 다른 누군가가 로딩했는지 찾아보고, 있다면 해당 클래스의 참조를 넘겨주는 것이다.

 

GPT가 아주 잘 만들어주었다

 

로딩되지 않았다면 밑 코드로 계속 내려간다.

중간중간에 다른 스레드가 이미 로딩을 완료하진 않았는지, 누가 이미 로딩을 수행 중인지, 현재 스레드가 락을 잡았는지 검사하는 코드가 길게 늘어서있는데 이를 모두 통과하면 그제서야 로딩을 시작한다.

    if (loaded_class == nullptr) {
      // Do actual loading
      // 메타스페이스에 넣을 InstanceKlass를 창조해냄
      loaded_class = load_instance_class(name, class_loader, THREAD);
    }

그러면 load_instance_class로 또 넘어가자.

 

//systemDictionary.cpp : 1300

InstanceKlass* SystemDictionary::load_instance_class(Symbol* name,
                                                     Handle class_loader,
                                                     TRAPS) {

  InstanceKlass* loaded_class = load_instance_class_impl(name, class_loader, CHECK_NULL);

  // If everything was OK (no exceptions, no null return value), and
  // class_loader is NOT the defining loader, do a little more bookkeeping.
  if (loaded_class != nullptr &&
      loaded_class->class_loader() != class_loader()) {

    ClassLoaderData* loader_data = class_loader_data(class_loader);
    check_constraints(loaded_class, loader_data, false, CHECK_NULL);

    // Record dependency for non-parent delegation.
    // This recording keeps the defining class loader of the klass (loaded_class) found
    // from being unloaded while the initiating class loader is loaded
    // even if the reference to the defining class loader is dropped
    // before references to the initiating class loader.
    loader_data->record_dependency(loaded_class);

    update_dictionary(THREAD, loaded_class, loader_data);

    if (JvmtiExport::should_post_class_load()) {
      JvmtiExport::post_class_load(THREAD, loaded_class);
    }
  }
  return loaded_class;
}

이 함수에서도 본격적인 로딩은 `load_instance_class_impl`에서 시작된다.

밑의 if 문에서는 인자로 받은 클래스 로더와 로딩된 클래스의 클래스 로더가 다른 경우를 분기한다.

당연히 다를 수 있다. 인자로 들어온 클래스 로더는 로딩을 요청한 클래스의 클래스 로더이기 때문이다.

이게 뭔 소리냐면... 다음 코드를 보자.

 

class A{
	void run(){}	
}

class B{
    public static void main(String[] args){
        A a = new A();
        a.run();
    }
}

여기서 클래스 B가 클래스 A의 인스턴스를 생성 중이다. 이 때는 B의 클래스 로더가 A의 로딩을 요청한다.

`load_instance_class`에서 인자로 받은 클래스 로더는 B의 클래스 로더이고, 로딩이 끝난 A 클래스의 클래스 로더랑은 당연히 다른 상황이 있을 수 있다.

 

// systemDictionary.cpp : 1158

InstanceKlass* SystemDictionary::load_instance_class_impl(Symbol* class_name, Handle class_loader, TRAPS){
if (class_loader.is_null()) { // 클래스 로더가 null이다? -> 부트스트랩 클래스로더 호출
...
	// 1237
    if (k == nullptr) { // 부트클래스로더
      // Use VM class loader
      PerfTraceTime vmtimer(ClassLoader::perf_sys_classload_time());
      k = ClassLoader::load_class(class_name, pkg_entry, search_only_bootloader_append, CHECK_NULL);
    }

    // find_or_define_instance_class may return a different InstanceKlass
    if (k != nullptr) {
      CDS_ONLY(SharedClassLoadingMark slm(THREAD, k);)
      k = find_or_define_instance_class(class_name, class_loader, k, CHECK_NULL);
    }
    return k;
...

`load_class_instance_impl`에서는 클래스 로더가 부트스트랩 클래스로더인지 아닌지에 따라 분기가 나뉜다.

클래스 로더에는 계층이 있다. jdk 9부터 모듈 시스템이 들어오면서 부트스트랩 클래스로더, 플랫폼 클래스로더, 애플리케이션 클래스로더로 나뉘는데 이는 나중에 포스팅하도록 하겠다.

위 분기에서 class_loader가 null이라는 건 부트스트랩 클래스로더가 이 클래스를 담당한다는 의미이다. 실제로 String같이 java.base 모듈에 속하는 클래스로더를 찍어보면 null 로 나온다.

 

} else {
    // Use user specified class loader to load class. Call loadClass operation on class_loader.
    ResourceMark rm(THREAD);

    JavaThread* jt = THREAD;

    PerfClassTraceTime vmtimer(ClassLoader::perf_app_classload_time(),
                               ClassLoader::perf_app_classload_selftime(),
                               ClassLoader::perf_app_classload_count(),
                               jt->get_thread_stat()->perf_recursion_counts_addr(),
                               jt->get_thread_stat()->perf_timers_addr(),
                               PerfClassTraceTime::CLASS_LOAD);

    // Translate to external class name format, i.e., convert '/' chars to '.'
    Handle string = java_lang_String::externalize_classname(class_name, CHECK_NULL);

    JavaValue result(T_OBJECT);

    InstanceKlass* spec_klass = vmClasses::ClassLoader_klass();

    // Call public unsynchronized loadClass(String) directly for all class loaders.
    // For parallelCapable class loaders, JDK >=7, loadClass(String, boolean) will
    // acquire a class-name based lock rather than the class loader object lock.
    // JDK < 7 already acquire the class loader lock in loadClass(String, boolean).
    JavaCalls::call_virtual(&result,
                            class_loader,
                            spec_klass,
                            vmSymbols::loadClass_name(),
                            vmSymbols::string_class_signature(),
                            string,
                            CHECK_NULL); // 자바 코드 점프

부트스트랩 클래스로더가 아니라면, `JavaCalls:call_virtual`을 통해 자바 코드로 점프하게 된다. 이때 점프하는 코드는 `java.lang.ClassLoader`의 `loadClass`이다.

이처럼 실질적으로 클래스로더가 담당하는 부분은 지금부터 시작이다. loadClass는 내부적으로 네이티브 메소드인 `defineClass1` 또는 `defineClass2`를 호출한다. 

다시 cpp코드로 넘어와서 defineClass 를 타고 넘어가다 보면 `create_from_stream`을 만나게 된다.

// klassFactory.cpp : 170

InstanceKlass* KlassFactory::create_from_stream(ClassFileStream* stream,
                                                Symbol* name,
                                                ClassLoaderData* loader_data,
                                                const ClassLoadInfo& cl_info,
                                                TRAPS) {
... // 198
  ClassFileParser parser(stream,
                         name,
                         loader_data,
                         &cl_info,
                         ClassFileParser::BROADCAST, // publicity level
                         CHECK_NULL); // 파싱 및 바이트코드 검증         

  const ClassInstanceInfo* cl_inst_info = cl_info.class_hidden_info_ptr();
  InstanceKlass* result = parser.create_instance_klass(old_stream != stream, *cl_inst_info, CHECK_NULL);
  // 이 코드에서 실제 InstanceKlass를 제작함
  // 위에서 생성자로 만들어낸 parser를 활용해서 제작함!! 
...

여기서는 `ClassFileParser`를 생성하는데, 이 코드 내에서 파싱 및 .class의 바이너리 스트림을 검증한다.

그렇다면 클래스 로더는 검증의 일부 또한 책임지는 것이다.

`ClassFileParser`의 `create_instance_klass` 함수를 다시 호출한다.

// classFileParser.cpp : 4994
InstanceKlass* ClassFileParser::create_instance_klass(bool changed_by_loadhook,
                                                      const ClassInstanceInfo& cl_inst_info,
                                                      TRAPS) {
  if (_klass != nullptr) {
    return _klass;
  }

  InstanceKlass* const ik =
    InstanceKlass::allocate_instance_klass(*this, CHECK_NULL);
...

이 함수에서는 `allocate_instacne_klass`를 또 호출하는데... 이름을 보아하니 메모리에 클래스 정보를 할당하고, 그 포인터를 돌려주는것같다.

// instanceKlass.cpp : 456
InstanceKlass* InstanceKlass::allocate_instance_klass(const ClassFileParser& parser, TRAPS) {
  const int size = InstanceKlass::size(parser.vtable_size(),
                                       parser.itable_size(),
                                       nonstatic_oop_map_size(parser.total_oop_map_count()),
                                       parser.is_interface());  
  
  // Allocation
  // 여기서 메타스페이스 영역에 할당
  if (parser.is_instance_ref_klass()) {
    // java.lang.ref.Reference
    ik = new (loader_data, size, THREAD) InstanceRefKlass(parser);
  } else if (class_name == vmSymbols::java_lang_Class()) {
    // mirror - java.lang.Class
    ik = new (loader_data, size, THREAD) InstanceMirrorKlass(parser);
  } else if (is_stack_chunk_class(class_name, loader_data)) {
    // stack chunk
    ik = new (loader_data, size, THREAD) InstanceStackChunkKlass(parser);
  } else if (is_class_loader(class_name, parser)) {
    // class loader - java.lang.ClassLoader
    ik = new (loader_data, size, THREAD) InstanceClassLoaderKlass(parser);
  } else {
    // normal
    ik = new (loader_data, size, THREAD) InstanceKlass(parser);
  }
 ...
 return ik;
}

여기서 또 들어가면? new로 확실히 할당을 한 후에 이를 돌려주고있다. 그런데 보니까 클래스 종류에 따라 다르게 할당을 해주고 있는것을 볼 수 있다.

new 오퍼레이터는 cpp에서 오버로딩할 수 있는데, 오버로딩을 통해 메타스페이스에 할당하는 코드로 바꾼것을 볼 수 있다.

void* Klass::operator new(size_t size, ClassLoaderData* loader_data, size_t word_size, TRAPS) throw() {
  return Metaspace::allocate(loader_data, word_size, MetaspaceObj::ClassType, THREAD);
}

해석은 lazy하게 일어나도 된다

링크는 검증, 준비, 해석 단계로 나뉜다. 아까 검증 단계는 일부 초기화 단계에서 수행되었다.

조금 더 알아보고 넘어가면 좋을 부분이 있는데, 해석 단계는 꼭 초기화보다 먼저 수행되지 않아도 된다.

해석 단계는 상수 풀의 심볼을 주소로 바꾸는 작업이다.

그런데 해당 심볼이 아직 사용되지도 않는데 미리 해석할 필요가 있을까? 미리 해석할 경우에는 로딩 -> 해석 -> 로딩 -> 해석의 무한 굴레가 발생하며 애플리케이션 전체에 대한 해석이 발생하여 초기 구동에 심각한 영향을 줄 것이라고 예상된다. (아마도요)

 

따라서 해석은 lazy하게 일어나도 된다고 한다. 필요한 시점이 오면 그 때 하면 된다는 뜻이다.

그러면 왜 초기화 이전에 해석이 일어난다고 하는걸까?

다음과 같은 경우가 있을 수 있다.

class A {
    static {
        B.hello();
    }
}

A를 초기화하는 도중에 B의 hello를 실행해야 하는데, 이 B와 hello에 대한 심볼릭 참조 해석이 필요할 수 있기 때문이다.

 

준비 단계는... 가상 테이블을 초기화하는 부분은 찾았는데, 초기값을 세팅하는 부분은 아직 찾지 못했다. 이 부분은 다음에 올리도록 하겠다.

 

초기화는 어디에서?

// interpreterRuntime.cpp : 214
JRT_ENTRY(void, InterpreterRuntime::_new(JavaThread* current, ConstantPool* pool, int index))
  Klass* k = pool->klass_at(index, CHECK);
  InstanceKlass* klass = InstanceKlass::cast(k);

  // Make sure we are not instantiating an abstract klass
  klass->check_valid_for_instantiation(true, CHECK);

  // Make sure klass is initialized
  klass->initialize(CHECK); // InstanceKlass::initialize

  oop obj = klass->allocate_instance(CHECK);
  current->set_vm_result_oop(obj);
JRT_END

아까 이 코드에서 initialize를 호출하는 부분을 봤을 것이다.

여기로 한번 들어가보자.

//instanceKlass.cpp : 809

void InstanceKlass::initialize(TRAPS) {
  if (this->should_be_initialized()) {
    initialize_impl(CHECK);
    // Note: at this point the class may be initialized
    //       OR it may be in the state of being initialized
    //       in case of recursive initialization!
  } else {
    assert(is_initialized(), "sanity check");
  }
}

여기서는 해당 클래스가 초기화되었는지 아닌지 플래그를 확인하고, 필요하다면 `initialize_impl`을 호출해서 초기화를 수행한다.

// instanceKlass.cpp : 1176
void InstanceKlass::initialize_impl(TRAPS) {
  HandleMark hm(THREAD);

  // Make sure klass is linked (verified) before initialization
  // A class could already be verified, since it has been reflected upon.
  link_class(CHECK);
  ...

`initialize_impl`에서는 초기에 `link_class`를 호출하는데, `link_class`에서는 내부적으로 가상 테이블을 초기화하는 준비 작업을 거친다. 이로 인해 실행 엔진이 링크 단계를 일부 담당하는것을 확인할 수 있다.

 

...
  // Step 7
  // Next, if C is a class rather than an interface, initialize it's super class and super
  // interfaces.
  if (!is_interface()) {
    Klass* super_klass = super();
    if (super_klass != nullptr && super_klass->should_be_initialized()) {
      super_klass->initialize(THREAD);
    }
    // If C implements any interface that declares a non-static, concrete method,
    // the initialization of C triggers initialization of its super interfaces.
    // Only need to recurse if has_nonstatic_concrete_methods which includes declaring and
    // having a superinterface that declares, non-static, concrete methods
    if (!HAS_PENDING_EXCEPTION && has_nonstatic_concrete_methods()) {
      initialize_super_interfaces(THREAD);
    } // 디폴트 메소드가 있다면 인터페이스 초기화 진행

...
  }

이 함수에서는 정말 긴 검사 스텝을 거치는데, 스텝을 일부 소개하면 

  • 누군가가 이미 초기화중인가?
  • 이미 초기화가 완료되었는가?
  • 이미 실패한 초기화인가?
  • 내가 초기화를 수행할 권한을 얻었는가?

등등을 검사한다. 이를 모두 거친 다음 위 코드에 도착하면, 슈퍼 클래스가 존재하는지 확인하고 슈퍼 클래스를 먼저 초기화한다. 그리고 인터페이스는 원래 초기화를 진행하지 않는데, 해당 인터페이스에 디폴트 메소드가 있다면 초기화를 수행한다.

  // Step 8
  {
    DTRACE_CLASSINIT_PROBE_WAIT(clinit, -1, wait);
    if (class_initializer() != nullptr) {
      // Timer includes any side effects of class initialization (resolution,
      // etc), but not recursive entry into call_class_initializer().
      PerfClassTraceTime timer(ClassLoader::perf_class_init_time(),
                               ClassLoader::perf_class_init_selftime(),
                               ClassLoader::perf_classes_inited(),
                               jt->get_thread_stat()->perf_recursion_counts_addr(),
                               jt->get_thread_stat()->perf_timers_addr(),
                               PerfClassTraceTime::CLASS_CLINIT);
      call_class_initializer(THREAD);
    } else {
      // The elapsed time is so small it's not worth counting.
      if (UsePerfData) {
        ClassLoader::perf_classes_inited()->inc();
      }
      call_class_initializer(THREAD);
    }
  }
...

그리고 모든 검사 스텝을 통과하면 `call_class_initalizer`를 호출한다.

void InstanceKlass::call_class_initializer(TRAPS) {
  if (ReplayCompiles &&
      (ReplaySuppressInitializers == 1 ||
       (ReplaySuppressInitializers >= 2 && class_loader() != nullptr))) {
    // Hide the existence of the initializer for the purpose of replaying the compile
    return;
  }

...

  methodHandle h_method(THREAD, class_initializer()); // <clinit>() 가져오기
...
  if (h_method() != nullptr) {
    ThreadInClassInitializer ticl(THREAD, this); // Track class being initialized
    JavaCallArguments args; // No arguments
    JavaValue result(T_VOID);
    JavaCalls::call(&result, h_method, &args, CHECK); // Static call (no args)
  }
}

여기서는 또 `JavaCalls::call`을 사용하고 있다. 클래스로더의 `loadClass`를 호출할때는 `callVirtual`이었는데, 여기서는 왜 그냥 call 일까... 생각해봤는데, 일반 함수를 호출하는 바이트코드는 invokeVirtual이고 생성자를 호출하는 함수는 invokeSpecial이라 그런것 같다. 아님말고

여기서 호출하는 메서드는 <clinit>이다. 이 메서드는 자바 컴파일러가 자동으로 넣어주는 메서드인데, 스태틱 필드와 스태틱 블록을 하나로 합쳐서 만들어진다. 여기서 진정한 초기화가 이루어지는 것이다.

 

이렇게 클래스 로더가 모든 과정을 관장한다는 것은 틀렸다는 것을 확인했다. 너무 블로그만을 믿지 말고 (내 블로그도) 직접 코드를 확인해보는 습관을 들이는게 좋을것 같다. 특히 openjdk 소스는 레퍼런스가 너무 없다보니까 AI도 헛소리를 엄청나게 뱉었다. 덕분에 조사가 더 어려워졌었다. (제미나이 바보)

'Java' 카테고리의 다른 글

상속과 오버라이딩에 관한 고찰  (0) 2026.05.10
JVM 튜?닝?  (1) 2025.11.27
자바 가비지 컬렉터 (GC)  (0) 2025.09.07
JVM  (0) 2025.09.06
SOLID 원칙  (0) 2025.09.05
정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차