Java/JVM

[JVM 밑바닥까지 파헤치기] 7-2. 클래스 로딩 매커니즘: 클래스 로더

noahkim_ 2024. 12. 23. 18:51

저우즈밍 님의 "JVM 밑바닥까지 파헤치기" 책을 정리한 포스팅 입니다

 

1. 클래스 동치 조건

  • 같은 FQCN + 클래스 로더로 로딩되어야 동일한 타입임
  • ✅ 서로 다른 클래스 로더로 같은 .class를 로드하면 완전히 다른 타입으로 취급됨
  • ➡️ 충돌 방지를 위한 모듈 격리를 위해 설계됨

 

예시) WAS

더보기

ex) 각 서버의 웹앱이 쓰는 라이브러리 종류가 같을 경우

  • 클래스의 FQCN은 같으나, 버전은 다를 수 있음
  • ⚠️ 만약 클래스로더 격리가 없다면, JVM에 클래스는 하나만 올라올 수 있어서 먼저 로드된 버전만 사용될 수 있음
  •  웹앱별로 격리하여 서로 다른 버전을 동시에 사용할 수 있음

 

2. 클래스 로더

  • .class 바이트코드를 읽어 메모리에 올리는 주체
  • ✅ 직접 정의 가능

 

코드) Application ClassLoader 정의

더보기

ClassLoader의 findClass() 또는 loadClass() 오버라이딩

// 간단한 커스텀 로더: 지정한 디렉토리에서 .class 바이트를 읽어 defineClass
static class MyClassLoader extends ClassLoader {
    private final Path baseDir;

    MyClassLoader(Path baseDir, ClassLoader parent) {
        super(parent);          // ✅ 부모를 AppClassLoader로 둠
        this.baseDir = baseDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // name: com.example.Hello -> com/example/Hello.class
        Path classFile = baseDir.resolve(name.replace('.', '/') + ".class");
        try {
            byte[] bytes = Files.readAllBytes(classFile);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("Cannot load " + name + " from " + classFile, e);
        }
    }
}
public class Main {
    public static void main(String[] args) throws Exception {
        ClassLoader app = ClassLoader.getSystemClassLoader(); // AppClassLoader
        Path dir = Path.of("classes"); // 예: ./classes/com/example/Hello.class 가 있어야 함

        ClassLoader my = new MyClassLoader(dir, app);

        Class<?> hello = Class.forName("com.example.Hello", true, my);
        System.out.println("Loaded by: " + hello.getClassLoader());
        System.out.println("App loader: " + app);
    }
}

 

계층 구조

종류 설명 특징
Bootstrap 최상위 부모
JVM 내장 (C++로 구현됨)
- 경로: JAVA_HOME/lib or -Xbootclasspath
- 예: JDK 기본 클래스 (java.lang.*, java.util.*)
Extension(Platform) JDK 확장 라이브러리 로드 - 경로: lib/ext or java.ext.dirs
- 예: sun.misc.Launcher$ExtClassLoader
Application(App) 사용자 작성 클래스 로드 - 경로: 사용자 클래스패스(classpath)
- 예: sun.misc.Launcher$AppClassLoader
  • 상위 클래스 로더에게 먼저 위임, 실패하면 자기 자신이 로드 시도
  • ✅ 자식 → 부모, ❌ 부모 → 자식
  • ➡️ 중복 로딩 방지 (동일 클래스가 여러번 로드되어선 안됨)
  • ➡️ 보안성 향상

 

예시) 보안성 향상

더보기
  • 만약 자식이 Java SE를 대체할 수 있다면, 악의적인 기본 클래스를 구현해서 JVM을 공격할 수 있음
  • 부모 위임 덕분에 java.* 패키지는 무조건 Bootstrap이 먼저 로드함

 

3. 예외 상황

기본 API에서 SPI를 호출할 경우

  • SPI: 프레임워크가 인터페이스를 정의하고, 사용자가 구현체를 제공하는 구조
  • ✅ JDK(부모 ClassLoader)가 SPI 인터페이스를 가지고 있음
  •  구현체는 사용자 클래스패스(자식 ClassLoader)에 있음
  • ⚠️ 부모 → 자식 접근 불가 원칙 때문에 로딩이 실패할 수 있음
  • ➡️ 스레드 컨텍스트 클래스 로더로 우회하기

 

예시) 스레드 컨텍스트 클래스 로더로 우회하기

더보기

부모 로더 (JDK 또는 상위 모듈에 있다 가정)

// MyService.java
public interface MyService {
    void execute();
}

 

자식 로더 (사용자 클래스패스에 있다 가정)

// MyServiceImpl.java
public class MyServiceImpl implements MyService {
    @Override
    public void execute() {
        System.out.println("MyServiceImpl 실행됨");
    }
}

 

호출

public class LoaderTest {

    public static void main(String[] args) throws Exception {

        // 현재 스레드의 컨텍스트 클래스 로더 가져오기
        ClassLoader cl = Thread.currentThread().getContextClassLoader();

        // ✅ 해당 로더로 클래스 로딩
        Class<?> clazz = Class.forName("MyServiceImpl", true, cl);
        // ❌ 이렇게 하면 부모 로더 기준으로 찾이서 접근이 불가함
        // Class<?> clazz = Class.forName("MyServiceImpl", true);

        MyService service =
                (MyService) clazz.getDeclaredConstructor().newInstance();

        service.execute();
    }
}
  • 부모가 찾기 말고, 현재 실행 중인 스레드가 들고 있는 클래스 로더를 사용하도록 하기
  • ➡️ 사용자 정의 클래스 로더를 명시적으로 전달해 자식 로더의 클래스를 접근 가능하게 만듦