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