이 글은 Java Virtual Machine을 학습 중 Class Loader에 관해 깊이 이해하고 싶어 작성한 글입니다. (Java8을 기준으로 하였습니다)
참고자료 : https://www.baeldung.com/java-classloaders
참고자료 : https://www.theserverside.com/tutorial/Classloaders-Demystified-Understanding-How-Java-Classes-Get-Loaded-in-Web-Applications
참고자료 : 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/
클래스 로더는 JVM이 실행되는 동안 JVM에 자바 클래스를 동적으로 로딩하는 책임이 있다. 즉, 클래스 로더는 JVM에 자바 클래스를 로딩하는 역할을 하는 대상을 말한다. JVM이 동작하기 위해 클래스 로더는 필수적인 존재이며, 클래스 로더는 JRE(Java Runtime Environment)에 속한다. 덕분에 JVM은 Java Program을 실행하기 위해 파일 시스템 구조를 알 필요가 없다.
추가적으로, 자바 클래스들은 JVM 메모리에 한번에 모두 적재되지는 않는다. 즉, 해당 클래스가 런타임에 필요해지는 순간에 메모리에 적재된다. 이제 궁금증이 생길 수 있다. 클래스 로더는 어떻게 클래스를 가져올까? 클래스 로더는 어떻게 구현되었을까?
클래스 로더에 관해 구체적으로 알아보기 전에, 간단한 예제를 보려고 한다.
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ PrintClassLoader.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
위 메소드는 메소드가 호출된 클래스를 호출하는 클래스로더, Logging클래스(com.sun.javafx.util.Logging)를 호출하는 클래스로더, ArrayList클래스(java.util.ArrayList)를 호출하는 클래스로더가 누구인지 출력하고 있다.
출력값은 다음과 같다.
Classloader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Classloader of Logging:sun.misc.Launcher$ExtClassLoader@1540e19d
Classloader of ArrayList:null
각 클래스를 불러오는 클래스 로더가 다른 것을 알 수 있다. 출력된 클래스 로더는 "AppClassLoader", "ExtClassLoader", "Bootstrap(as null)"이다. 글의 뒤에서 자세히 설명할 것이지만, 위 클래스 로더들은 BootStrap <- extension <- application 순으로 상속관계를 가지고 있다. 따라서 할아버지인 BootStrap부터 손주인 application 순으로 설명하고자 한다.
자바 클래스들은 java.lang.ClassLoader에 의해 로드된다. 그런데, 이때 ClassLoader들 또한 자바 클래스이다! 그럼 도대체 ClassLoader 클래스를 로드하는 ClassLoader는 누구일까? 고놈이 바로 BootStrap Class Loader이다.
BootStrap 클래스 로더는 JDK의 내부 클래스들을 로드하는 책임이 있다. 내부 클래스라 함은, "rt.jar"와 "$JAVA_HOME/jre/lib" 디렉토리에 위치한 core library들을 말한다. 추가적으로 위 클래스 로더는 다른 ClassLoader 인스턴스들의 부모로서 역할을 한다. 즉, 모든 클래스 로더의 시초는 BootStrap 클래스 로더이다.
"$JAVA_HOME/jre/lib"
"rt.jar"
위와 같은 역할을 수행하기 위해 BootStrap 클래스 로더는 Java로 작성되어서는 안된다. Java Program이 실행되기 위해 JVM이 먼저 실행되어야 하는데, 만약 BootStrap이 Java로 작성된 클래스라면 말이 안되는 상황이 되기 때문이다. 이 때문에, BootStrap 클래스 로더는 Native 언어(ex. C or C++)로 작성되있으며, JVM이 동작하는 플랫폼 환경에 따라 다르게 구현되있다.
'그럼 BootStrap 클래스 로더는 누가 실행하냐'라는 궁금증이 든다면, 아래 링크를 참조하기 바란다.
링크 : https://stackoverflow.com/questions/18214174/how-is-the-java-bootstrap-classloader-loaded
요약하자면, BootStrap 클래스 로더는 Machine Code(ex. C or C++)로 작성되 있어서 JVM이 실행될 때 가장 우선적으로 실행되어 전체 클래스 로딩 프로세스를 실행시키는 역할을 한다. Machine Code로 작성되 있기 때문에, JVM(혹은 클래스 로더)의 도움없이 플랫폼 내부적으로 실행 될 수 있다.
extension 클래스 로더는 Bootstrap 클래스 로더의 자식이다. 그리고 이 클래스 로더는 standart core java class들의 extension 클래스들을 로드한다. 이 클래스 로더는 JDK extension 디렉토리($JAVA_HOME/lib/ext)에 있는 클래스들을 로드한다. 이때 extension 클래스라 함은, core Java API를 확장하는 클래스들을 말한다. 더 깊이 알고 싶다면 Java의 Extension Mechanism을 찾아보면된다. (링크 : https://docs.oracle.com/javase/tutorial/ext/index.html)
"AppClassLoader"로 지칭된 Application 클래스 로더는 예제 메소드가 포함된 클래스를 로드한다. Application 혹은 System 클래스 로더는 어플리케이션 레벨의 클래스들을 JVM에 적재하는데 사용된다. 이때, 어플리케이션 레벨의 클래스란 class path에 놓여져 있는 클래스 파일들을 말한다. 즉, 개발자가 작성한 클래스들을 말하며, 위 클래스 로더는 Java 사용자가 만든 클래스 파일들을 로드하는데 사용된다. 이 클래스는 마찬가지로 extension 클래스 로더의 자식이다.
JVM이 클래스를 요청하면, 클래스 로더는 클래스의 위치를 특정하고 클래스의 정의를 런타임에 로드한다. "java.lang.ClassLoader.loadClass()" 메소드가 클래스를 로드하는데 사용되는 메소드이다. 그런데 만약 클래스가 미리 로드되있지 않다면, 해당 클래스 로더는 본인이 직접 클래스를 찾지않고 해당 클래스 요청을 부모 클래스 로더에게 위임한다. 이 프로세스는 재귀적으로 발생한다.
특별하게, 부모 클래스 로더가 클래스를 찾지 못한다면, 자식 클래스는 "java.net.URLClassLoader.findClass()"메소드를 호출하여 파일 시스템에서 해당 클래스를 직접 찾는다. 만약 마지막 자식 클래스 로더도 클래스를 찾지 못할 경우, "java.lang.NoClassDefFoundError" or "java.lang.ClassNotFoundException"을 throw하게 된다.
아래 예시는 ClassNotFoundException을 throw하는 출력문이다.
java.lang.ClassNotFoundException: HelloWorld
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:264)
at classloader.SampleClassLoader.classNotFoundExceptionTest(SampleClassLoader.java:5)
at classloader.Main.callSampleClassLoader(Main.java:20)
at classloader.Main.main(Main.java:5)
첫번째로 "java.lang.Class.forName()" 메소드를 호출하여 클래스를 찾도록 한다. 이때 해당 메소드에서는 본인의 클래스 로더인 Application 클래스 로더가 아닌, 부모 클래스 로더가 해당 클래스를 찾는 것을 볼 수 있다. 이후, 부모 클래스 로더가 클래스를 찾지 못하여 Application 클래스 로더의 "loadClass()"메소드가 호출되고 "java.net.URLClassLoader.findClass()"가 호출되는 것을 볼 수 있다. 최종적으로 클래스를 찾지 못하여 "ClassNotFoundException"을 throw하게 된다.
위와 같은 클래스 로더의 동작을 이해하기 위해 클래스 로더의 중요한 특징들을 알아보자.
클래스 로더는 Delegation Model을 따른다. Delegation Model이라 함은, 클래스를 찾는 요청이 들어왔을 때 클래스 로더 인스턴스가 해당 요청을 부모 클래스 로더에게 위임함을 의미한다.
만약 Application level의 클래스를 JVM에 적재하는 요청이 들어왔다고 가정하자.
- Application 클래스 로더는 클래스 로드 요청을 부모 클래스 로더인 Extension 클래스 로더에게 위임한다.
- Extension 클래스 로더는 클래스 로드 요청을 부모 클래스 로더인 BootStrap 클래스 로더에게 위임한다.
- Bootstrap 클래스 로더가 가장 조상인 클래스 로더임으로 이제 클래스 로딩을 본격적으로 시작한다. rt.jar에서 해당 클래스가 있는지 확인한다.
- Bootstrap 클래스 로더가 찾지 못했으면, Extension 클래스 로더가 extension 클래스가 담긴 폴더에서 해당 클래스를 찾는다.
- Extension 클래스 로더가 찾지 못했으면, Application 클래스 로더가 클래스패스에서 해당 클래스가 있는지 찾아본다. Application 클래스 로더의 실제 구현체는 URLClassLoader이며, URLClassLoader는 classpath에서 해당 클래스가 있는지 확인한다.
- 최종적으로 Application 클래스 로더에서 클래스로 로드하지 못했을 경우, ClassNotFoundException을 throw한다.
Delegation Model의 결과로, JVM에 로드되는 클래스들이 중복되지 않고 유일함을 보장하는 것은 쉬워진다. 왜냐하면 클래스를 로드할 때 항상 상위 클래스 로더에게 요청을 위임하기 때문이다. 만약 상위 클래스 로더에서 클래스를 발견하였다면, 자식 클래스 로더에서 중복되게 클래스를 로드할 경우없이 상위 클래스 로더에서 이를 알려줄 것이다.
하위 클래스 로더는 상위 클래스 로더가 로드한 클래스들을 볼 수 있다. 하지만, 상위 클래스 로더는 하위 클래스 로더가 로드한 클래스를 볼 수 없다. 이렇게 하는 이유는, 하위 클래스 로더인 Application 클래스 로더에서 "String.class"와 같이 상위 클래스 로더가 로드한 클래스를 볼 수 있어야만 어플리케이션이 정상적으로 동작할 수 있기 때문이다.
반면, 상위 클래스 로더가 하위 클래스 로더가 로드한 클래스를 보지 못하는 이유는 상,하위 구분을 명확히 하기 위함으로 추측된다. 클래스 로더를 3가지 구조로 나눠 각각의 역할을 명확히 하기위해 visibility 또한 동일하여야 한다.