ClassLoader

2024-06-02

entrance

Intro

Bytecode를 담고 있는 .Class파일은 JVM(Java Virtual Machine)ClassLoader에 의해 해석되어 실행됩니다. 어떤 과정을 거쳐서 우리가 작성한 클래스들이 해석되어 메모리에 로딩되는지 알아보겠습니다.

ClassLoader

JVMClassLoaderBoostrap ClassLoader, Extension ClassLoader, Application ClassLoader로 구성된 3계층 구조로 이뤄져있습니다. (~JDK8) 이 3계층 구조에서 각 클래스 로더가 특정 역할에 맞는 클래스를 로딩하도록 설계되어 있습니다.

BootStrap ClassLoaderJVM의 네이티브 코드로 구현된 클래스로더입니다. BootStrap ClassLoaderClassLoader는 외부 클래스로더로 구분합니다. JAVA_HOME/lib디렉토리나 -Xbootclasspath 매개 변수로 지정한 경로에 위치한 파일과 JVM 클래스 라이브러리를 로드하는 일을 책임집니다. rt.jar를 로드합니다.

Extension ClassLoaderJDK에 필요한 확장 라이브러리(java 3D, JavaMail 등)를 로드합니다. JAVA_HOME/lib/ext에 있는 클래스를 로드합니다.

Application ClassLoader는 애플리케이션에 필요한 클래스를 로드합니다. ClassPath에 지정된 경로에서 클래스를 로드하며 애플리케이션의 진입점인 main메서드를 포함한 클래스를 로드합니다. 우리가 작성한 코드를 로드하는 것이 Application ClassLoader의 역할입니다.

ClassLoader 설명 로드 경로
BootStrap ClassLoader - JVM의 기본 동작에 필요한 코드를 로드합니다.
- Java ClassLoader를 로드합니다.
- rt.jar에 속한 클래스를 로드합니다.
- 최상단의 ClassLoader입니다.
JVM 기본 클래스 경로
Extension ClassLoader - BootStrap의 자식입니다.
- JDK에 필요한 라이브러리를 로드합니다.
- jre/lib/ext 디렉토리를 로드합니다.
- 시스템 속성(system property)을 설정합니다.
jre/lib/ext 디렉토리
Application ClassLoader - 애플리케이션에 필요한 클래스를 로드합니다.
- ClassPath에 기재된 클래스를 로드합니다.
- 애플리케이션의 진입점인 main 메서드를 포함한 클래스를 로드합니다.
사용자 지정 클래스 경로

ClassLoader는 클래스를 로딩할 때 부모 ClassLoader에게 먼저 클래스를 로드할 것을 위임합니다. 그리고 부모 ClassLoader가 해당 클래스를 로딩할 수 있는지를 확인하고 클래스를 로드하지 못하면 findClass()로 클래스를 로드하게 됩니다.

Bootstrap Class Loader ↑ Extension Class Loader ↑ Application Class Loader

이렇게 부모 ClassLoader가 클래스를 로딩하도록 위임하는 것은 동일한 클래스파일을 로딩하더라도 로드한 ClassLoader가 다르면 JVM에서 다른 클래스로 인식되어 여러개의 클래스 파일이 존재하게 되기 때문입니다.
(ClassLoader는 클래스를 로드한 다음에 각 클래스의 타입정보에는 어떤 ClassLoader가 로드한 것인지를 기록합니다.)

ClassLoader에 의해서 클래스가 로딩되면 해당 클래스의 타입정보메서드 영역에 저장합니다. 그리고 java.lang.Class라는 객체를 생성하여 메서드 영역에 저장된 클래스의 타입 정보를 가리키도록 설정한 후 자바의 Heap 메모리에 저장합니다. 이 Class객체는 프로그램에서 메서드 영역 안의 타입 데이터에 접근하기 위한 통로 역할을 합니다.

ClassLoading

classLoading ClassLoader에 의해 로드된 클래스들을 동적 링킹 과정을 거쳐 초기화됩니다. C++언어는 컴파일 타임에 링킹을 하는 정적 링킹인 반면 Java런타임에 링킹을 하기 때문에 애플리케이션 구동 초기에 오버헤드가 있는 편입니다. 하지만 이러한 동적 링킹 덕분에 클래스 동적 로딩 / Lazy 로딩이 가능하며 Bytecode는 플랫폼에 독립적일 수 있습니다.

링킹 단계는 3단계로 구성됩니다. Verify -> Prepare -> Resolve 각 단계는 병렬적으로 수행이 가능합니다. 위 순서는 시작시점을 기준으로 나타냅니다.

Verify
검증단계에서는 클래스 파일의 바이트스트림에 담긴 정보가 JVM 명세에서 규정하는 제약을 만족하는지 확인합니다. 클래스 파일이 꼭 java코드로부터 컴파일되어야 하는 것은 아닙니다. 바이너리 편집기로 바이트스트림을 작성할 수도 있습니다. 그래서 만약에 악의적으로 작성된 바이트코드 스트림이 로드될 수도 있기 때문에 JVM명세에 만족하는지 검증합니다.

  • 파일 형식 검증
    • 바이트스트림의 시작이 매직넘버인가?
    • 메이저 버전마이너버전이 현재 JVM이 허용하는 범위에 속하는가?
    • 존재하지 않는 상수나 타입을 가리키는 경우는 없는가?
  • 메타데이터 검증
    • 상위 클래스 또는 인터페이스에서 정의한 필수 메서드를 모두 구현했는가?
    • 필드와 메서드가 상위 클래스와 충돌하는가?
  • 바이트코드 검증
    • 메서드 본문인 Code속성에서 StackMapTable의 속성이 잘못 되었는가?
  • 심벌 참조 검증
    • 클래스 파일에 존재하는 심벌 참조가 올바른가?

검증단계에서 파일 형식 검증을 완료한 후 메서드 영역에 클래스 타입 정보를 저장합니다. 앞서 클래스로더클래스파일을 로딩하면 메서드 영역타입 정보가 저장된다고 말씀드렸는데 로딩 단계와 링킹의 검증 단계는 어느 정도 겹쳐있습니다.

Prepare
준비단계에서는 클래스 변수(정적 변수)를 메모리에 할당하고 디폴트값을 설정합니다. 클래스 변수클래스 객체와 함께 Heap에 저장되고 이때 ConstantValue속성을 제외한 변수는 0과 같은 디폴트값으로 초기화 됩니다.

// final로 선언된 경우에만 100으로 할당됩니다. // final은 바이트코드에서 ConstantValue속성을 입니다. public static final int value = 100;

Resolve
해석단계에서는 심벌 참조직접 참조로 대체하는 과정입니다. 이 단계에서는 필드, 메소드, 다른 클래스인터페이스를 참조할 때 사용되는 심벌(이름)이 해당하는 실제 메모리 위치로 연결합니다. 심벌 참조는 클래스 파일 내 상수 풀에 저장되어 있습니다.

> javap -v -c .\Bytecode.class Classfile /java/main/org/jovialcode/base/Bytecode.class Last modified 2024. 5. 12.; size 444 bytes MD5 checksum 241941b8981b9e32e706593708b84fcb Compiled from "Bytecode.java" public class org.jovialcode.base.Bytecode minor version: 0 //마이너 버전 major version: 62 //메이저 버전 flags: (0x0021) ACC_PUBLIC, ACC_SUPER // 접근 제어자 this_class: #9 //Bytecode 클래스 인덱스 // org/jovialcode/base/Bytecode super_class: #2 //Bytecode가 상속하는 클래스 // java/lang/Object interfaces: 0, fields: 1, methods: 2, attributes: 1 Constant pool: //상수풀 //#1 Methodref는 [java/lang/Object] 클래스의 [<init>메소드]를 가리킵니다. #1 = Methodref #2.#3 // java/lang/Object."<init>":()V //#2 Class는 [java/lang/Object]클래스를 나타내는 [심벌참조]입니다. #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V

Initializing

초기화 단계에서는 클래스변수초기값이 할당됩니다. 이전에 준비단계에서는 클래스변수기본값으로 0이 할당되어 있었습니다. 클래스변수초기값을 할당하기 위해서 클래스 생성자인 <clinit> 메서드가 실행됩니다. <clinit> 메서드는 클래스 변수 할당과 정적 문장 블록(static {})이 실행되는데 이 코드는 컴파일러에 자동으로 생성되는 부분입니다.

Outro

초기화단계까지 마치면 클래스 파일이 JVM 로드되어 우리가 작성한 대로 애플리케이션이 구동됩니다.