1. JVM 뜯어보기 - Class Loader

2025. 5. 14. 20:54·Java
728x90

JVM (Java Virtual Machine)

JVM은 크게 세 가지 subsystem으로 나뉜다.

  1. Class Loader
  2. Runtime Data Areas
  3. Execution Engine

1. Class Loader

  • 동적 로딩 (Dynamic Loading) : 필요한 바이트 코드만을 로딩한다.
    • 컴파일시점이 아닌 실행 시점에 클래스를 로딩할 수 있게 해주는 기술이다.
    • 자바에서 동적 로딩이 가능한 이유가 바로 Class Loader 덕분이다.
  • 바이트 코드(.class 파일)을 JVM에 적재
  • Loading → Linking → Initialization 을 차례대로 수행함

2. Runtime Data Areas

  • JVM이 프로그램 실행을 위해 OS로부터 할당 받은 메모리 영역
    • 모든 스레드 공유(프로그램 전역) : Heap, Method Area(Metaspace), Shared Libs
    • 스레드 마다 독립적 : Java Stack, Native Method Stack, PC Register

3. Executor Engine

1. Java Interpreter

  • 자바 컴파일러(javac)에 의해 변환(소스 코드 → 바이트 코드)된 바이트 코드를 읽고 해석
  • 인터프리트 방식은 C나 C++에서 하는 것처럼 프로그램 실행 전 한 번의 컴파일을 거치는 대신, 런타임에 필요한 부분을 즉석으로 해석하는 방식이다.

2. JIT Compiler

  • 핫스팟(자주 실행되는 코드)을 실행 중인 OS 및 하드웨어에 맞게 기계어로 변환하여 Native Area에 존재하는 Code Cache 영역에 캐싱
  • 다음 요청 시 바이트 코드를 해석하는 대신 캐싱된 기계어가 실행된다.

3. Garbage Collector

  • 더 이상 사용하지 않는 메모리를 자동으로 회수해준다. 개발자가 따로 메모리 관리를 하지 않아도 되므로, 더욱 손쉽게 프로그래밍 할 수 있도록 해준다.
  • Full GC가 발생하는 경우, Stop-the-world (GC 제외 모든 쓰레드 중지)로 장애가 발생할 수 있다.

https://dev.to/deepu105/visualizing-memory-management-in-jvm-java-kotlin-scala-groovy-clojure-19le

https://www.artima.com/insidejvm/ed2/jvm2.html

https://loosie.tistory.com/847

https://chamalwr.medium.com/inside-the-jvm-part-05-execution-engine-15857827ac82

 

Class Loader

JVM의 Class Loader는 클래스들과 인터페이스들(이하 클래스 메타데이터)에 동적으로 loading, linking, initializing을 수행한다.

 

클래스들은 요청이 들어올 때만 로드되며, 각각의 클래스들은 오직 한 번만 로드된다.

 

각 클래스 로더는 클래스들을 보관하는 네임스페이스를 갖는다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾는다. FQCN이 같더라도 네임스페이스가 다르면, 다른 클래스로 간주된다.

 

 

1. Loading

  • Loading은 javac에 의해 컴파일된 .class 파일의 바이트 코드를 메모리에 로딩하는 단계를 말한다.
  • 바이너리 코드 형태로 로드되지만, 특정 하드웨어에 귀속된 기계어를 말하는 것은 아니다. (중간언어를 바이너리 코드로만 변환한 것)
  • 클래스 / 인터페이스는 이진 이름(binary name) + 이를 로드한 클래스 로더 쌍에 의해 결정된다.
  • 만약 로딩 단계에서 실패하게 된다면, 이 클래스 / 인터페이스를 사용할 때 에러를 던지게 된다. (directly or indirectly)

2. Linking

  • Linking은 Verification → Preparation → Resolution의 3단계로 구성된다.

2 - 1. Verification

load된 클래스나 인터페이스의 이진 표현이 정상적인 구조로 이루어졌는지 검증한다. 이 과정에서 추가적으로 클래스나 인터페이스가 로드될 수 있지만, 이들에 대해서는 verification이나 preparation 하지 않는다. Verification 실패 시 VerifyError 가 발생한다.

  • 만약 verify 시도 시 LinkageError를 상속하는 에러(VerifyError와 같은) 인스턴스가 발생한다면, 그 이후의 과정들에 대해서도 이와 동일한 에러에 의해서 항상 실패하게 된다.

2 - 2. Preparation

클래스나 인터페이스의 static field들을 생성하고 타입별 default 값으로 설정한다. 이 과정에서 JVM의 코드 실행은 필요하지 않다. 또한 default 값이 아닌 특정한 값으로의 초기화는 Preparation 단계가 아닌, Initialization 단계에서 일어난다. 또한 이 단계에서 loading constraints를 설정하기도 한다.

 

2 - 3. Resolution

anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, ldc2_w, multianewarray, new, putfield, and putstatic 등의 JVM 명령어들은 runtime constant pool을 참조한다. 따라서 이러한 명령어들의 실행을 위해서는 symbolic link의 resolution이 필요하다.

  • Symbolic Reference는 실제 물리주소와 연결되며, 이러한 작업이 바로 Resolution이다. (Symbolic Reference -> Direct Reference)
    • 초기 resolution이 필요한 경우(static, static final)에는 클래스 로더에 의해서 resolution 발생하며, 그렇지 않은 경우(인스턴스 메서드 호출)에는 런타임에 JVM 런타임 시스템에 의해 lazy resolution이 발생한다.
  • 공식 문서에 따르면, runtime constant pool에 있는 하나 이상의 symbolic reference를 구체 값으로 동적 결정하는 과정 자체를 resolution이라고 한다.
  • 초기 모든 runtime constant pool에 존재하는 symbolic reference들은 unresolved 상태이다.
    • (i) 클래스 혹은 인터페이스, (ii) 필드, (iii) 메서드, (iv) 메서드 타입, (v) 메서드 핸들, (vi) 동적 계산되는 상수 들에 대해서 resolution이 진행된다.
    • Resolution 단계에서 에러가 발생하지 않는다면, resolution이 성공적으로 완료된 것이다. 같은 symbolic reference에 대한 후속적인 resolution 시도는 보통 항상 성공하며, 반환되는 결과는 초기 resolution에서 생산된 entity와 동일하다.
    • 만약 symbolic reference가 동적 계산되는 상수라면, 후속적인 resolution 시도에 대해서 bootstrap method의 재실행은 이루어지지 않는다.
  • symbolic reference의 resolution 단계에서 발생하는 에러는 IncompatibleClassChangeError를 상속하는 에러 인스턴스이거나, resolution 또는 bootstrap method 실행 중 발생한 Error 인스턴스이거나, 클래스 로딩 실패 혹은 loading constraints 위반으로 발생한 LinkageError의 인스턴스일 수 있다.
    • 이러한 에러들은 프로그램에서 해당 symbolic reference를 사용할 때 반드시 던져지게 된다.(directly or indirectly)
    • 에러가 발생한 symbolic reference에 대한 후속적인 resolution 시도는 항상 실패하며, 초기 resolution 시도에서 발생한 결과와 동일한 에러가 던져진다.
    • 만약 symbolic reference가 동적 계산되는 상수라면, 후속적인 resolution 시도에 대해서 bootstrap method의 재실행은 이루어지지 않는다.

3. Initializing

  • JVM은 multi thread 운용이 가능하므로, 동시에 클래스 혹은 인터페이스를 초기화 하려는 시도 혹은 재귀적으로 초기화 하려는 시도가 있을 수 있다. 멀티 스레딩 환경에서는 동기화에 주의해야 한다. 따라서 Initialization 단계 전에 클래스 혹은 인터페이스는 반드시 link 되어있어야 한다. 이 말은 곧, verification, preparation, 부분적 resolution을 완료해야만 Initialization이 가능하다는 말과 같다.
  • Linking > Preparation 에서 만들고 default value로 초기화 했던 static field에 대해서 concrete value(명시된 값)으로 설정한다. <clinit> 실제 코드를 실행하여 명시된 초기값을 설정하며, <clinit> 은 클래스가 최초로 참조될 때 단 한 번만 실행된다.
    • static final 필드에 compile-time constant(컴파일 타임 상수)가 들어가면, 다른 클래스에서 이 값을 사용할 때, <clinit> 을 호출하지 않으며(참조 x), Runtime Constant Pool에 상수값이 복사되어 들어가게 된다.

 

클래스 로더의 특징

  • 클래스 로더는 계층적(Hierarchical)이다.
    • Bootstrap ← Extension(= Platform) ← System ← UserDefined
    • 부모 - 자식 관계의 계층적 구조
    • 클래스 로더는 계층적 구조를 가지도록 생성이 가능하고, 각 부모 클래스 로더에서 자식 클래스 로더를 가지는 형태로 클래스 로더를 만들 수 있다.

1 . 클래스 로더는 위임형 로드 요청 원칙(Delegate Load Request Principle)을 갖고 있다.

  • 특정 클래스 로더에서 클래스 로드 요청이 들어오면, 부모 클래스 로더에게 재귀적으로 해당 로드 요청을 위임한다.
  • 만약 System Class Loader에 A라는 클래스 로드 요청이 들어온다면,
    • System Class Loader → Extension Class Loader → Bootstrap Class Loader(최상위 클래스 로더) 요청 위임
  • 위임된 요청에 의해, 최상위 클래스 로더부터 거슬러 내려오며 클래스를 찾는다. 만약 특정 계층의 클래스 로더가 가 클래스 A를 찾게되면(로드) 더 이상 자식 클래스 로더에 요청을 전달하지 않고 찾은 클래스를 리턴한다. 리턴된 클래스는 최초 호출자 Class Loader까지 전달된다.
    • 최초 호출자 Class Loader에서도 클래스 A를 찾지 못 한다면, Exception이 발생한다.

2. 클래스 로더는 가시범위 원칙(Visibility Principle)을 갖고 있다.

  • 하위 클래스 로더는 상위 클래스 로더가 로딩한 클래스를 볼 수 있지만, 상위 클래스로더는 하위 클래스 로더가 로딩한 클래스를 볼 수 없다.
  • 부모 클래스 로더가 동일한 클래스 로더 간에도 서로가 로딩한 클래스를 볼 수 없다.

3. 클래스 로더에 로드된 클래스는 기본적으로는 언로드가 불가능(Cannot Unload Classes)하며 유일성 원칙(Uniqueness Principle)을 가진다.

  • 클래스 로더에 의해 로딩된 클래스들은 다시 JVM 상에서 없앨 수 없다.
    • 클래스 단위의 언로드 불가능
    • 특정 클래스 로더가 로드한 모든 메타 데이터들이 더이상 참조되지 않는 경우라면, GC가 발생할 수 있다. 이 역시 클래스 로더 단위로만 가능하며, 클래스 단위의 언로드는 불가능하다.
    • 따라서 클래스 로더 자체를 삭제하고, 새로운 클래스 로더를 생성하여 새로 로딩하는 방식만 가능하다.
  • 유일성 원칙에 따라서 하위 클래스 로더는 상위 클래스 로더가 로딩한 클래스를 다시 로딩하지 않는다.

 

클래스 로더 종류

JAVA9부터 Project Jigsaw(직쏘 프로젝트)가 도입되어 모듈 시스템이 적용되었다. 이에 따라 클래스 로더의 구조에도 상당 부분 변화가 있었다.

 

1. Bootstrap Class Loader

JVM의 내장 클래스로더로 최상위 클래스로더이다. Primodial Class Loader라고도 한다. 다른 클래스 로더와 달리 자바가 아닌 네이티브 코드(Native C)로 구현되어 있다.

  • 따라서 부트스트랩 클래스 로더에 의해 로드된 클래스는, 클래스 로더 출력 시 null 출력
  • String.class.getClassLoader() // 실행 시, null 출력. ArrayList.class.getClassLoader() // 실행 시, null 출력.
  • ~JAVA8 : $JAVA_HOME/jre/lib/rt.jar 의 JAVA SE 표준 API들과 $JAVA_HOME/jre/lib 에 포함된 기타 핵심 라이브러리들 로드.
    • 즉, 모든 Java 기본 클래스들을 로드
  • JAVA9 ~ : /rt.jar 이 사라지고, 내부 내용들이 모듈화되어 /lib/modules 내에 저장됨. 이에 따라Bootstrap Class Loader가 로딩하는 클래스 범위가 줄어들었다. ****
    • 즉, 최소한의 핵심 모듈인 java.base 만 로드 (모든 모듈이 의존하는 기본 모듈이다)

2. Platform Class Loader

  • ~ JAVA8 : Extension Class Loader라는 명칭. jre/lib/ext 내부 클래스 및 java.ext.dirs 환경변수로 지정된 폴더에 있는 클래스 파일 로드. URLClassLoader 를 상속받아 sun.misc.Launcher 내부 static 클래스로 구현됨.
  • JAVA9 ~ : Platform Class Loader로 명칭 변경됨. jre/lib/ext , java.ext.dirs 를 지원하지 않음. BuiltinClassLoader 를 상속받아 ClassLoaders 클래스의 내부 static 클래스로 구현됨. 부트스트랩 클래스 로더에서 로드한 핵심 모듈(java.base)를 제외한 Java SE Platform APIs들과 구현 클래스들, JDK-specific runtime 클래스들과 같은 플랫폼 클래스들을 로드한다.

3. System Class Loader

  • ~ JAVA8 : Application Class Loader라는 명칭. -classpath (또는 -cp) 나 JAR 파일 안에 있는 Manifest 파일의 Class-Path 속성값으로 지정된 폴더에 있는 클래스를 로딩한다. URLClassLoader 를 상속받아 sun.misc.Launcher 내부 static 클래스로 구현됨. 개발자가 애플리케이션 구동을 위해 직접 작성한 대부분의 클래스는 이 클래스 로더에 의해 로딩된다.
  • JAVA9 ~ : 클래스패스, 모듈패스에 있는 클래스 로드. BuiltinClassLoader를 상속받아 ClassLoaders 클래스의 내부 static 클래스로 구현됨.

 

동적 호출 매커니즘 (MethodHandle, Reflection)

  • method handle : 동적 호출 매커니즘. 정적인 메서드 호출이 아닌, 동적으로 메서드 호출 가능. 런타임에 동적 바인딩이 이루어진다.
    • MethodHandle.invoke() : Object로 처리하여 유연한 동적 호출. 유연하게 처리하지만 검사 비용 발생
    • MethodHandle.invokeExact() : 정확한 타입 기반의 동적 호출. 타입이 맞지 않으면 컴파일 에러 또는 런타임 에러 발생. 컴파일 시점에 타입 결정. 검사가 실행 시점이 아닌, 생성 시점에 이루어져서 검사 비용을 줄임.
  • Reflection 또한 동적 호출을 지원한다. 하지만 reflection은 Object 기반의 느슨한 타입 체킹으로 많은 런타임 검사와 오버헤드를 가지게 된다. 런타임 유연성은 매우 높은 편이며, MethodHandle과 달리 JIT 최적화가 불가능하다.

 

https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-5.html

https://brewagebear.github.io/fundamental-jvm-classloader/

https://engkimbs.tistory.com/606

https://homoefficio.github.io/2018/10/13/Java-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%A1%9C%EB%8D%94-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0/#search

 

다형성 (PolyMorphism)

  • 다형성은 하나의 객체가 여러 타입을 가질 수 있는 것을 의미한다.
  • Reference Type의 경우 상속이 가능하지만, Value Type (⇒ 구조체, 열거형)은 상속 불가
  1. 정적 (Compile Time) 다형성
    • 오버로딩 (Overloading) : 동일한 이름의 메서드가 파라미터 타입과 개수에 의해서 구분됨
  2. 동적 (Runtime) 다형성
    • 오버라이딩 (Overriding) : 부모 클래스에서 정의된 메서드를 자식 클래스에서 재정의하여 서로 다른 작동을 구현하는 것
  • 일반적으로 OOP에서 말하는 다형성은 동적 다형성 ⇒ 오버라이딩이다.

다형성 개념은 다음에 설명할 method dispatch와도 연관되어 있다.

https://wikidocs.net/274925

 

자바 메서드 호출 과정과 dispatch

  • Method Dispatch : 어떤 메서드를 호출할 지 결정하고, 실행시키는 과정을 의미한다.
    • 주로 OOP에서 다형성 연산의 어떤 구현을 선택하게 될 때 Method Dispatch가 발생한다. 동일한 이름의 메서드가 존재(OverLoading, Overriding)하는 경우, 해당 메서드의 구현이 어떤 객체로부터 발생했는지에 대해 결정짓고 실행하는 것이다.
      • → 동일한 이름의 메서드가 여러개 존재 : 메시지 위임 가능
    • Static Dispatch (Direct Call) : 컴파일 시점에 메서드 호출에 대해서 특정된 변수 타입 및 메서드를 런타임에 그대로 실행한다. 동적 디스패치에 비해 성능 상 이점이 있다.
      • 정적 디스패치는 오버라이딩이 적용되지 않는 경우 발생한다.
        1. 오버로딩 (컴파일 타임 다형성)
          • 언어마다 다를 수 있으나, 자바의 경우 오버로딩도 정적 디스패치
        2. 정적 메서드 (정적 메서드는 오버라이딩 불가능)
          • 컴파일 시점에 클래스 종속적으로 메모리 할당됨. 객체와 무관하게 동작
          • 정적 메서드의 경우 자식 메서드에서 재정의를 시도하면 overriding 대신 hiding으로 동작하게 된다.
        3. final, private 메서드 (오버라이딩 불가능)
        4. Value Type ⇒ 구조체, 열거형 (상속 불가능)
      • 혹은 변수 타입이 구체적으로 지정된 경우에도 정적 디스패치 발생
        • 다형성을 사용하지 않는 경우이다.
    • Dynamic Dispatch (Indirect Call) : 런타임에 함수가 호출되고, 파라미터 타입들이 특정되면, 이에 맞는 적절한 객체와 메서드를 특정 짓고, 실행한다.
      • 동적 디스패치는 오버라이딩이 적용된 경우 발생한다.
      • 런타임에 vTable(Virtual Table)을 사용한다.

https://softwareengineering.stackexchange.com/questions/200115/what-is-early-and-late-binding/200123#200123

  • Binding, Dispatch, Loading에 관해서 잘 구분해 설명된 글
    • Binding : Type
    • Dispatch : Method (Function)
    • Loading : Value

https://jinyoungchoi95.tistory.com/16

https://youngkdevlog.tistory.com/79

  • Swift 관련 글이긴 하지만, OOP Method Dispatch 관련하여 잘 설명된 글

https://medium.com/@devjohnpark/java-method-call-process-and-dispatch-cded3f2c3103

728x90

'Java' 카테고리의 다른 글

2. JVM 뜯어보기 - Java Runtime Data Area  (2) 2025.05.14
0. JVM 기초와 자바의 컴파일러 및 인터프리터에 관하여  (0) 2025.05.14
'Java' 카테고리의 다른 글
  • 2. JVM 뜯어보기 - Java Runtime Data Area
  • 0. JVM 기초와 자바의 컴파일러 및 인터프리터에 관하여
suhsein
suhsein
티끌모아 태산
  • suhsein
    기억 못 할 거면 기록이라는 좋은 수단이 있다
    suhsein
  • 전체
    오늘
    어제
    • 분류 전체보기
      • ASAC
      • Next.js
      • Docker
      • MySQL
      • Java
      • Spring-Proxy, AOP
      • Spring Boot, JPA
      • Spring Security
      • DB
      • 알고리즘
      • PS
      • TOPCIT
      • AWS 자격증
      • 비공개
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 안녕하세요
  • 인기 글

  • 태그

    동적프로그래밍
    Alias
    포인터
    오블완
    DP
    티스토리챌린지
    tsp
    외판원순회
    해시
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
suhsein
1. JVM 뜯어보기 - Class Loader
상단으로

티스토리툴바