Gun.Kim Back-end Developer

04.Class Loader

Java는 런타임 시에 필요한 클래스를 동적으로 로드하는 기능을 제공한다. 이러한 동적 로딩 메커니즘은 Java의 핵심적인 특징 중 하나이며, 이를 통해 JVM이 클래스의 참조가 발생할 때마다 필요한 클래스를 동적으로 로드하고 연결(Link)하여 애플리케이션에서 사용할 수 있도록 한다. 이 과정은 ClassLoader라는 모듈을 통해 이루어진다.

ClassLoader란?

ClassLoader는 JVM 내로 클래스를 로드하고, 로드된 클래스들을 애플리케이션이 사용할 수 있도록 배치하는 일련의 작업을 수행하는 모듈이다. ClassLoader는 클래스의 이름과 이 클래스를 로드한 ClassLoader의 이름을 기반으로 클래스를 구별하여 동일한 클래스를 중복 로드하지 않도록 한다. 이 과정은 각 ClassLoader가 자신만의 Namespace를 가지고, 로드한 클래스의 정보를 저장하고 관리함으로써 가능하다.

Dynamic Loading

Java의 Dynamic Loading은 클래스 로딩 시점에 따라 두 가지로 나눌 수 있다

  1. Load Time Dynamic Loading: 애플리케이션이 시작될 때 관련된 모든 클래스를 한꺼번에 로드하는 방식이다. 이는 주로 기본 Java API를 포함한 클래스를 로드할 때 사용된다.

    예를 들어, 다음 코드는 Hello 클래스가 시작될 때 String 객체와 System 클래스를 함께 로드하는 것을 보여준다.

    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello EXEM");
        }
    }
    

    위 코드에서 Hello 클래스가 JVM에 의해 로드될 때, java.lang.Stringjava.lang.System 클래스도 동시에 로드된다. 이는 Load Time Dynamic Loading의 전형적인 예이다.

  2. Runtime Dynamic Loading: 애플리케이션 실행 중에 특정 클래스를 참조할 때 그 클래스를 동적으로 로드하는 방식이다. 주로 Reflection이나 Spring 같은 프레임워크에서 사용된다.

    다음 코드는 런타임 시에 클래스를 동적으로 로드하는 방법을 보여준다.

    public class Hello {
        public static void main(String[] args) {
            try {
                Class cl = Class.forName(args[0]);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    

    위의 코드에서는 Class.forName(args[0])를 호출하여 런타임에 인수로 전달된 클래스 이름에 해당하는 클래스를 로드한다. 이 방식은 어떤 클래스가 로드될지를 애플리케이션이 실행되기 전까지 알 수 없기 때문에, 실행 중에 필요한 클래스를 로드하는 유연성을 제공한다.

Namespace

Namespace는 Java에서 ClassLoader가 클래스를 로드할 때 사용하는 개념으로, JVM 내에서 클래스의 고유성을 보장하는 역할을 한다. 이는 JVM이 동일한 클래스를 중복해서 로드하지 않도록 로드된 클래스의 정보를 저장하고 관리하는 각각의 ClassLoader의 Namespace를 통해 이루어진다.

Namespace의 역할

  • 클래스 식별: ClassLoader는 클래스의 Full Qualified Name(패키지 이름과 클래스 이름)과 해당 클래스를 로드한 ClassLoader의 이름을 함께 사용하여 클래스의 고유성을 식별한다. 즉, ClassLoaderName + PackageName + ClassName이 모두 동일해야만 JVM은 그것을 같은 클래스로 인식한다.

  • 중복 로드 방지: 각 ClassLoader는 자신만의 Namespace를 가지고 있어, 자신이 로드한 클래스의 정보를 저장한다. 이를 통해 JVM은 클래스가 이미 로드되어 있는지를 확인하고, 중복 로드를 방지할 수 있다.

예시와 동작

Namespace의 개념을 이해하기 위해 두 개의 ClassLoader가 동일한 클래스(exem.package.jvmclass)를 로드하려는 상황을 고려해 보자.

Namespace 예시

  • ClassLoader 1exem.package.jvmclass를 로드하고 나서, 그 정보를 자신의 Namespace에 저장한다.
  • ClassLoader 2는 동일한 클래스 이름을 로드하려고 하지만, 자신의 Namespace를 확인했을 때 해당 클래스가 로드되지 않았음을 발견하고, 클래스 로드를 수행한다.

이로 인해 같은 JVM 내에서도 클래스가 서로 다른 ClassLoader에 의해 중복 로드될 수 있다. 따라서 각 ClassLoader의 Namespace는 독립적이며, 자신이 로드한 클래스만 관리하게 된다.

Symbolic Reference와의 관계

ClassLoader가 Namespace를 검색할 때 사용하는 주요 메커니즘 중 하나는 Symbolic Reference이다. Symbolic Reference는 객체의 이름으로 참조를 할 때마다 ClassLoader가 해당 클래스를 로드해야 할지 여부를 결정하기 위해 Namespace를 참조하는 방법이다.

class ExemHello {
    public static void main(String[] args) {
        Class cl = Class.forName("exem.package.jvmclass");
    }
}

위의 코드를 살펴보면 ExemHello 클래스가 exem.package.jvmclass를 참조할 때, JVM은 이 클래스를 로드해야 하는지 결정하기 위해 이미 로드된 클래스와의 관계를 검토한다.

  • Java에서는 클래스 참조 시 동일한 ClassLoader를 사용해야 한다는 규칙이 있다. 이는 클래스의 일관성과 안정성을 보장하는 데 중요하다.

이와 같은 특성 때문에 여러 Web Application Server(WAS)는 ClassLoader Tree를 구성하여 사용하며, 각 Application이 독립적으로 구성되도록 관리한다.

Symbolic Reference 예시

ClassLoader Delegation Model

JVM 내에는 여러 개의 ClassLoader가 있으며, 이들은 계층 구조를 이루고 있다. ClassLoader Delegation Model은 이러한 계층 구조를 기반으로 서로에게 임무를 위임하는 방법을 말한다.

  • Bootstrap Class Loader: 가장 상위의 ClassLoader로, 부모가 없으며 JVM이 기동할 때 기본적으로 생성되며 Runtime 환경 구성하는 기초 단계를 수행한다. $JAVA_HOME/jre/lib/rt.jar를 로드하고, Java의 핵심 API들을 로드하며, 네이티브 코드로 구현되어 있다.

  • Extension Class Loader: Bootstrap Class Loader를 부모로 하며, $JAVA_HOME/jre/lib/ext에 위치한 확장 클래스들을 로드한다. Bootstrap, Extension Class Loader는 JVM을 위한 Class Loader라고 볼 수 있다.

  • Application Class Loader: 사용자 애플리케이션의 클래스를 로드하며 User Defined Class Loader와 결합되어 사용된다. $CLASSPATH 또는 java.class.path에 위치한 class들을 로드한다.

    • User Defined Class Loader는 애플리케이션에서 직접 생성이 가능하며 주로 WAS나 프레임워크에서 생성하여 사용하는 경우가 많다.

Delegation Model

ClassLoader Delegation Model은 로드 요청을 위계적으로 전달하는 방식이다. 대부분의 ClassLoader는 자신이 로드를 수행하기보다는 부모 ClassLoader에게 먼저 위임하여 이미 로드된 클래스를 검색한다.

이러한 위임 체계는 클래스의 중복 로드를 방지하고, 클래스의 일관성을 유지하는 데 도움을 준다. 예를 들어, 한 애플리케이션에서 Application Class Loader가 클래스를 로드하도록 요청하면, 이 요청은 먼저 ExtensionClassLoader, 그다음에는 BootstrapClassLoader로 위임되어 탐색한다.

Class Sharing

Class Sharing은 Java 5에서 도입된 기능으로, 여러 JVM 프로세스 사이에서 이미 로드된 클래스를 공유하는 기능이다. 이를 통해 JVM의 기동 시간을 줄이고, 메모리 사용을 효율적으로 관리할 수 있다.

Class Sharing의 동작 방식

  • Hotspot JVM에서는 File 형태 Class 또는 Jar 파일을 최초 로드한 JVM은 이를 Memory Mapped File 형태로 만들어 놓는다. 이를 Shared Archive라고 한다. Shared Archive File은 JVM을 위해 Shared Library 형태로 저장되며 jar 대신 jsa 확장자를 사용한다.

Class Sharing은 JVM이 rt.jar와 같은 기본 클래스 파일을 미리 로드해 두면, 다른 JVM에서 이를 공유하여 로드 시간을 절약하고 메모리 사용을 최소화하는 데 도움을 준다.

Class Loader Work

Class Loader는 파일 형태의 클래스를 런타임 메모리에 Type으로 만드는 작업을 수행한다. 이 과정을 Class Loader Work라고 하며, Loading, Linking, Initialization의 세 가지 과정으로 나뉜다.

Loading

Binary 형태의 Type을 JVM 안으로 가지고 오는 과정을 의미하며 세가지 과정으로 이루어진다.

  • Acquisition: 파일 형태의 클래스를 JVM으로 획득하는 작업이다. 파일 시스템이나 네트워크 등 다양한 경로를 통해 클래스를 획득할 수 있다.
  • Parse: 획득한 클래스를 Method Area의 데이터 구조에 맞게 분석하여 메타 정보를 생성한다. 또한 JVM Crash를 방지하기 위해 파일 크기와 Super-Class의 존재 여부를 확인하는 등 Linking 과정의 Verification 작업의 일부를 수행하기도 한다.
  • Class 인스턴스 생성: java.lang.Class 인스턴스를 생성하여 Application과 JVM의 내부 Runtime Data 사이를 연결하는 Interface 역할을 한다. 이 class 객체는 모든 Class의 최상위 객체이기 때문에 Class를 JVM에 자리 잡게 하기 위해 반드시 필요하다.

Linking

Loading된 Type을 Runtime 상태 Binary Type Data로 구성하는 과정으로 세가지 과정으로 이루어진다.

  • Verification: 로드된 클래스가 Java 언어의 문법에 맞고, 프로그램 전개상 문제가 없는지를 검증한다. 중점적으로 다음 사항들을 검증한다.
  • Preparation: 검증이 완료된 클래스를 위한 메모리 할당과 Default 값을 설정한다.
  • Resolution: Constant Pool의 Symbolic Reference를 Direct Reference로 변경한다.

Initialization

Preparation 작업에서 설정된 Default 값을 실제로 사용할 값으로 변경한다. Initialization 과정이 완료되어야 클래스가 사용 가능하게 된다.