Class Loader
자바는 동적으로 클래스를 읽어오므로, 프로그램이 실행 중인 런타임에서야 모든 코드가 자바 가상 머신과 연결된다. 이렇게 동적으로 클래스를 로딩해주는 역할을 하는 것이 바로 JVM의 클래스 로더(class loader)라고 한다.
자바에서 소스를 작성하면 .java파일이 생성되고 .java소스를 컴파일러가 컴파일하면 .class파일이 생성되는데 클래스 로더는 .class 파일을 묶어서 JVM이 운영체제로부터 할당받은 메모리 영역인 Runtime Data Area로 적재한다. (같은 JVM 내에서 동작)
클래스 로더는 계층 구조를 가지고 있으며 System Class Loader는 Platform Class Loader를 부모로 가지고, Platform Class Loader는 Bootstrap Class Loader를 부모로 가진다
Java 8
3가지 기본 클래스로더
Bootstrap ClassLoader
- Bootstrap ClassLoader는 3가지 기본 클래스로더 중 최상위 클래스로더로서, 쉽게 말하면 jre/lib/rt.jar에 담긴 JDK 클래스 파일을 로딩한다. 즉, JVM 실행에 필요한 클래스들을 로딩
- Native C로 구현돼 있어서, String.class.getClassLoader()는 그냥 null을 반환한다. Primordial ClassLoader 라고 불리기도 한다.
Extension ClassLoader
- Extension ClassLoader는 시스템 속성 값인 jre/lib/ext 폴더나 java.ext.dirs 환경 변수로 지정된 폴더에 있는 클래스 파일을 로딩한다.
- jdk 확장 라이브러리들을 다룬다
- java.lang.ClassLoader의 인스턴스로 Java SE platform API 등 자바에서 기본적으로 제공해주는 클래스를 로딩할 때 사용된다.
- Java로 구현되어 있으며 sun.misc.Launcher 클래스 안에 static 클래스로 구현되어 있으며, URLClassLoader를 상속하고 있다.
- Bootstrap Class Loader를 부모로 가지고 있다.
Application ClassLoader
- Application ClassLoader는 -classpath(또는 -cp)나 JAR 파일 안에 있는 Manifest 파일의 Class-Path 속성값으로 지정된 폴더에 있는 클래스를 로딩한다.
- Extension ClassLoader와 마찬가지로 Java로 구현되어 있고, sun.misc.Launcher 클래스 안에 static 클래스로 구현되어 있으며, URLClassLoader를 상속하고 있다.
- 개발자가 애플리케이션 구동을 위해 직접 작성한 대부분의 클래스는 이 애플리케이션 클래스로더에 의해 로딩된다.
- Extension ClassLoader를 부모로 가지고 있다.
3가지 원칙과 클래스 로더 동작 방식
자바 클래스로더는 3이라는 숫자와 친해 보인다. 기본 클래스로더가 3가지이고, 작동 원칙도 3가지다.
Delegation Principle (위임 원칙)
위임 원칙은 클래스 로딩이 필요할 때 3가지 기본 클래스로더의 윗 방향으로 클래스 로딩을 위임하는 것을 말한다.
클래스 로더 동작 방식
클래스 로더의 동작 방식은 Java 9 에서도 동일하다. (클래스 로더 이름은 Java 8 기준으로 설명.)
클래스 로더는 기본적으로 lazy-loading 방식으로 동작한다. (lazy-loading은 실제로 해당 클래스가 사용될 때 까지 로딩을 지연하는 것을 뜻한다.)
JVM 프로세스가 실행되면 클래스 로더(Class Loader)가 애플리케이션에 사용되는 클래스들을 메모리로 로딩한다.
Bootstrap ClassLoader는 JVM start-up 시점에 클래스들을 메모리로 로딩한다.
Extension ClassLoader와 Application ClassLoader는 런타임에 해당 클래스가 사용될때 클래스를 메모리로 로딩한다.
클래스 로딩은 다음과 같은 절차로 동작한다.
main() 메서드가 포함된 ClassLoaderRunner 클래스에서 개발자가 직접 작성한 Internal 클래스를 로딩하는 과정을 그림으로 표현하면 다음과 같다.
- ClassLoaderRunner는 자기 자신을 로딩한 Application ClassLoader에게 Internal 클래스 로딩을 요청한다.
- 클래스 로딩 요청을 받은 애플리케이션 클래스로더는 Internal을 스스로 직접 로딩하지 않고 상위 클래스로더인 Extension ClassLoader에게 위임한다.
- 클래스 로딩 요청을 받은 Extension ClassLoader도 Internal을 스스로 직접 로딩하지 않고 상위 클래스로더인 Bootstrap ClassLoader에게 위임한다.
- 부트스트랩 클래스로더는 rt.jar에서 Internal을 찾아서
- 있으면 로딩 후 반환하고
- 없으면 Extension ClassLoader가 jre/lib/ext 폴더나 java.ext.dirs 환경 변수로 지정된 폴더에서 Internal을 찾아서
- 있으면 로딩 후 반환하고
- 없으면 Application ClassLoader가 classpath에서 Internal을 찾아서
- 있으면 로딩 후 반환하고
- 없으면 ClassNotFoundException이 발생한다.
이런 식으로 동작하는 이유는 두 번째 원칙인 Visibility Principle과 관련이 있다.
Visibility Principle (가시범위 원칙)
가시범위 원칙은 하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 볼 수 있지만, 상위 클래스로더는 하위 클래스로더가 로딩한 클래스를 볼 수 없다는 원칙이다.
만약에 개발자가 만든 클래스를 로딩하는 애플리케이션 클래스로더가 부트스트랩 클래스로더에 의해 로딩된 String.class를 볼 수 없다면 애플리케이션은 String.class를 사용할 수 없을 것이다. 따라서 하위에서는 상위를 볼 수 있어야 애플리케이션이 제대로 동작할 수 있다.
상위에서도 하위를 볼 수 있다면 상/하위 구분이 사실상 없어진다. 클래스로더를 3가지로 나눈 이유가 있을텐데 상위가 하위를 볼 수 있으면 구분 의미가 희석돼버린다.
따라서 하위에서는 상위를 볼 수 있지만 상위에서는 하위를 볼 수 없어야 한다.
Uniqueness Principle (유일성 원칙)
유일성 원칙은 하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 다시 로딩하지 않게 해서 로딩된 클래스의 유일성을 보장하는 것이다.
유일성을 식별하는 기준은 클래스의 binary name인데, toString()으로 찍다보면 가끔 보이는 java.lang.String, javax.swing.JSpinner$DefaultEditor, java.security.KeyStore$Builder$FileBuilder$1, java.net.URLClassLoader$3$1 이런 것들이 바로 binary name이다.
binary name의 자세한 내용은 https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.1 를 참고
.
Java 9
Java 9 에서도 기본 클래스로더의 3계층 구조와 3가지 원칙은 그대로 유효하다. 다만 Jigsaw 프로젝트로 Java 9 에서는 모듈 시스템이 도입되었다.
기존 JDK의 파일들이 작은 단위의 모듈로 분리되고, 더 효율적인 포맷으로 변환되었다. 또한 불필요한 파일들은 제거되기도 했다.
https://docs.oracle.com/javase/9/migrate/toc.htm#JSMIG-GUID-EEED398E-AE37-4D12-AB10-49F82F720027 요 내용 중 ClassLoader에 관련된 내용만 추려보면 다음과 같다.
Java 8 | Java 9 | 달라진 점 |
Bootstrap ClassLoader |
이름 그대로 | - rt.jar 등이 없어짐에 따라 로딩할 수 있는 클래스의 범위가 전반적으로 축소 - 따라서 parent classloader 인자로 null을 넘겨주며 Bootstrap ClassLoader를 parent classloader로 사용했던 코드 수정 필요할 수 있음 |
Extension ClassLoader |
Platform ClassLoader |
- jre/lib/ext, java.ext.dirs를 지원하지 않음 - Java SE의 모든 클래스와 Java SE에는 없지만 JCP에 의해 표준화 된 모듈 내의 클래스를 볼 수 있으며, Java 8에 비해 볼 수 있는 범위가 확장됨 - URLClassLoader가 아닌 BuiltinClassLoader를 상속받아 ClassLoaders 클래스의 내부 static 클래스로 구현됨 |
Application ClassLoader |
System ClassLoader |
- 클래스패스, 모듈패스에 있는 클래스 로딩 - URLClassLoader가 아닌 BuiltinClassLoader를 상속받아 ClassLoaders 클래스의 내부 static 클래스로 구현됨 |
rt.jar, tools.jar 가 제거됨
rt.jar, tools.jar 등 기본으로 제공되던 jar 파일이 없어지고 그 안에 있던 내용들은 모듈 시스템에 맞게 더 효율적으로 재편되어 lib 폴더 안에 저장된다. 이에 따라 rt.jar내의 모든 클래스를 로딩할 수 있던 Bootstrap ClassLoader가 로딩할 수 있는 클래스의 범위도 전체적으로 줄어들었다.
따라서 Bootstrap ClassLoader를 parent classloader로 사용하던 코드에서는 문제가 발생할 수 있다.
이럴 때는 Bootstrap Classloader를 의미하는 null 대신 Classloader.getPlatformClassLoader()를 인자로 넘겨서 가시 범위가 확장된 Platform ClassLoader를 parent classloader로 사용하면 된다.
jre/lib/ext, java.ext.dirs, lib/endorsed, java.endorsed.dirs 가 제거됨
jre/lib/ext, lib/endorsed 가 파일시스템에 존재하거나 java.ext.dirs, java.endorsed.dirs가 환경변수로 설정되어 있으면 javac나 java는 실행이 종료된다.
Class Loader의 특징 정리
- 위임 원칙
- 클래스 로더는 기본적으로 위임 모델을 채택한다. 자신에게 클래스 로딩 요청이 들어오면 자신의 부모 클래스 로더에게 클래스 로딩 요청을 보내고(위임) 부모 클래스 로더가 클래스를 찾지못하면 그 후에 자신이 클래스를 탐색한다.
- 가시 범위 원칙
- 하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 볼 수 있지만, 상위 클래스로더는 하위 클래스로더가 로딩한 클래스를 볼 수 없다. 이러한 계층 구조를 통해 클래스 로더의 책임은 분리하고 클래스 로더는 자신이 책임지는 클래스를 로딩할 수 있다.
- 유일성 원칙
- 하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 다시 로딩하지 않게 해서 로딩된 클래스의 유일성을 보장한다.
자세한 내용은 How the JVM Locates, Loads, and Runs Libraries, Class ClassLoader 에서 확인할 수 있다.
Class Loader 단계 : Loading, Linking, Initializing
JVM은 동적으로 로드, 링크, 초기화 과정을 진행한다. 로딩은 특정 이름을 가진 클래스 또는 인터페이스의 바이트 코드(.class)를 찾은 후 클래스 또는 인터페이스를 생성하는 과정이다.
JAVA 어플리케이션의 동작은 JVM을 시작한 후 특정 클래스를 Runtime Data Area으로 로딩한 후 로딩, 링크, 초기화 과정을 거쳐 최종적으로 특정 클래스의 public static void main(String []) 함수를 실행하는 것이다. 해당 과정을 실행하면서 연쇄적으로 다른 클래스들을 로딩, 링크, 초기화한다
즉, Class Loader는 .class를 JVM의 Runtime Data Area에 올려주고, 검증, 초기화 역할을 한다.
- Loading(로드) : 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
- Linking(링크) : 클래스 파일을 사용하기 위해 검증하고 기본값은 초기화하는 과정이다.
- Initialization(초기화) : 클래스 변수들을 적절한 값으로 초기화한다.
유의할점은, 클래스를 메모리에 올리는 Loading 기능은 한번에 메모리에 올리지 않고, 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다는 점이다.
사람들이 많이들 착각하는 점이 위의 3가지 과정이 거의 동시에 이루어져서 같이 묶어 생각하는데, 엄연히 클래스 로드(Loading)와 초기화(Initialization)은 다른 작업이다. (이는 뒤에서 자세히 다룰 것이다)
즉, JVM은 실행될때 모든 클래스를 메모리에 올려놓지 않고, 그때 마다 필요한 클래스를 메모리에 올려 효율적으로 관리하는 것이다.
JVM 시작
JVM이 시작되면 Runtime Data Area이 생성되고 그 안에 메소드, 힙 영역이 할당된다.
1. JVM에 내장된 BootStrap Class Loader는 java.lang.package 처럼 JVM 실행에 필요한 클래스들을 메소드 영역으로 로딩하고System Class Loader를 통해 실행한 클래스를 메소드 영역으로 로딩한다.
즉, 클래스 로드가 .class 파일을 읽고, 내용에 맞는 binary 데이터를 생성한 뒤 메모리의 Method영역에 저장한다.
- FQCN(Fully Qualified Class Name). 클래스가 속한 패키지명을 모두 포함한 이름
- 클래스, 인터페이스, Enum
- 각 클래스 / 인터페이스의 메소드, 변수
2. 로딩이 끝나면, 해당 클래스 타입의 class 객체를 생성하여 Heap영역에 저장.
- 객체이름.class 또는 인스턴스의 getClass() 형태로 호출했을 때 리턴되는 값을 말함.
- Class<객체이름> 형태.
로딩 (Loading)
클래스 또는 인터페이스의 생성은 해당 클래스의 필드, 메소드, 런타임 상수 풀 등 클래스가 가지고 있는 바이트 코드를 찾은 후 JVM의 메소드, 힙 영역에 구성하는 것을 의미한다. 클래스 로더를 통해 로딩을 진행하며 A 클래스를 로딩했을 때 A 클래스의 부모 클래스가 존재할 경우 먼저 부모 클래스를 로딩한다.
- 자바 바이트 코드(.class)를 메소드 영역에 저장한다.
- 각 자바 바이트 코드(.class)는 JVM에 의해 메소드 영역에 다음 정보들을 저장한다.
- 로드된 클래스를 비롯한 그의 부모 클래스의 정보
- 클래스 파일과 Class, Interface, Enum의 관련 여부
- 변수나 메소드 등의 정보
AppClassLoader는 개발자가 생성해서 classpath에 컴파일된 클래스 파일을 읽는 용도.
public class App {
public static void main(String[] args) {
ClassLoader classLoader = App.class.getClassLoader();
// AppClassLoader 리턴
System.out.println(classLoader);
// PlatformClassLoader 리턴
System.out.println(classLoader.getParent());
// null 리턴.
// Bootstrap Class Loader는 native로 구현되어 있기 때문에 자바에서 확인 불가.
System.out.println(classLoader.getParent().getParent());
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null
링크 (Linking)
링크는 검증(verification), 준비(Prepare), 분석(Resolution) 3가지 과정으로 이루어져 있다.
- 검증 : 로딩된 바이트 코드가 JVM 명세를 따르고 있는지 검증하는 과정
- 준비 : 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스를 나타내는 데이터 구조를 준비.
- Int type은 0으로, reference type은 null로 초기화 된다.
- 분석 : 클래스의 런타임 상수 풀 안에 있는 Symbolic Memory Reference를 실제 Reference(고정된 주소 값)로 바꾸는 과정(Optional과정)
- 예를 들어, Book book = new Book(); 이라는 코드가 있을 때, book이라는 참조 변수가 Heap에 저장된 실제 Book 클래스를 가리킬 수 있도록 연결하는 과정.
💡 심볼릭 레퍼런스란?
심볼릭 레퍼런스는 참조하는 대상의 이름을 지칭하고, 자바 바이트 코드(.class)가 JVM에 올라가게 되면 심볼릭 레퍼런스는 이름에 맞는 객체의 물리적인 주소를 찾아서 연결하는 작업을 수행한다.
검증, 준비, 분석 3가지 과정을 거치면서 다른 클래스의 로딩을 추가적으로 요청할 수 있다. 이 때 분석 과정은 검증, 준비 과정과 같은 시간에 일어날 필요가 없다. 보통 Symbolic Reference를 고정된 주소 값으로 변환시키는 분석 과정은 해당 명령이 실행될 때 일어난다.
예시) 메소드 오버라이딩
오버라이딩 된 함수는 실행 시점에 해당 함수를 호출하는 메세지와 함수가 분석과정을 통해 연결된다. 그렇다면 어떤 기준으로 함수를 선택하는 것일까?
During execution of an invokeinterface or invokevirtual instruction, a method is selected with respect to (i) the run-time type of the object on the stack, and (ii) a method that was previously resolved by the instruction.
invokeinterface 또는 invokevirtual 이라는 바이트 코드와 특정 메소드를 연결할 때 , 스택 최상단에 올라와 있는 객체의 타입에 따라 메소드를 결정한다.
즉 Java의 동적 바인딩은 실행 시점에 클래스의 런타임 상수 풀에 있는 Symbolic Referenc를 고정된 주소 값으로 바꾸는 것이며 이 때 고정된 주소 값을 선택하는 기준은 스택 위에 올라와 있는 객체의 타입이다.
자세한 내용은 The Java® Virtual Machine Specification의 5.4.5 Method Overriding에서 확인할 수 있다.
초기화(Initialization)
클래스 초기화 함수를 실행한다. 클래스의 static으로 선언된 변수와 메소드에 메모리를 할당, 초기값을 채우는 과정이다.
static final String name = "staticName"; 코드가 클래스에 있다면, 여기서 초기화된다.
초기화 과정은 로딩-검증-준비 과정이 모두 끝났을 때 한번만 실행된다.
JVM 종료
일부 스레드가 Runtime 클래스의 종료 메서드나 중지 메서드, 클래스 시스템의 종료 메서드를 호출하면 JVM 종료 또는 중지 작업이 Security Manager에 의해 허용된다.
ClassLoader를 왜 알아둬야 할까?
예시 중에 하나로 우리가 만든 웹, 앱이 WAS(Tomcat에 war 파일 형태로 서비스를 제공할 경우) 서버에 올라가는 경우가 있을 것이다.
- 이 경우 메인 메소드는 웹앱 서버(WAS)가 통제하고, 내 애플리케이션이 메인 메소드 내에서 실행되는 구조.
그런데 서비스를 배포한 뒤, 코드를 수정해서 재배포해야 하는 경우가 생겼다고 가정하자.
이때 클래스 로더로 클래스를 메모리에 올리고 나면, 클래스 정보를 지우는 방법은 프로그램을 종료하는 방법 외에는 따로 없다.
( 배포해서 서비스 중인 웹앱을 클래스를 수정하려고 종료하면...? 말이 안되지...)
그래서 클래스를 변경했을 때, 변경된 클래스가 적용되도록 하기 위해 User Defined Class Loader가 필요하다. ex) Tomcat ClassLoader구조
특히 상위 클래스 로더에서 읽어들인 클래스일수록, 나중에 클래스를 변경하기 어렵다.
참고