* 2023.05.24 내용 추가
Part1에서는 JVM이 무엇인지 간략하게 알았고,
Part2에서는 JVM을 구성하는 중요한 3가지 중 Execution Engine(실행엔진)을 살펴보았다.
실행엔진은 Runtime Data Area에 할당된 ByteCode(바이트코드)를 실행시키는 주체라고 했다.
이번에는 JVM 구성 요소 중 바이트코드를 가져오는 ClassLoader(클래스로더)에 관한 글이다.
📄 다시 한 번 간단하게 복습해보자
JDK에서 개발하고, JRE에서는 자바를 개발하지 않고 실행만 하는 환경이라고 했다.
JVM은 JRE의 중요한 요소로써 자바를 실행하기 위한 가상 머신이다.
JVM은 사용자 언어인 자바와 기계어 사이의 중간 언어인 자바 바이트코드를 사용한다
🤔 그렇다면 JVM에 Class는 어떻게 탑재될까?
Java Source File(자바 소스파일)이 Java Compiler(자바 컴파일러)에 의해 바이트코드로 변환된다.
변환된 바이트코드들은 클래스로더를 통해서 Runtime Data Area에 저장된다.
클래스로더 안에서는 Loading, Linking, Initialization 단계를 거치게 된다.
이 과정을 좀 더 자세히 살펴보자
자바 컴파일러를 통해서 컴파일된 바이트코드(.class 확장자를 가진 클래스 파일)는 각 디렉터리에 흩어져 있다.
또한, 기본적인 라이브러리의 클래스 파일들은 $JAVAHOME_내부경로에 존재한다.
각각의 클래스 파일들을 찾아서 JVM 메모리에 동적으로 탑재해주는 역할을 하는 것이 바로 클래스로더의 역할이다.
더 자세히 말하자면 Runtime Data Area(JVM 메모리)의 Method Area에 적재하는 것이다.
그 뿐만 아니라, 클래스로더는 크게 Loading, Linking, Initialization 역할도 있다.
- Loading : 클래스 파일을 탑재하는 과정
- Linking : 클래스 파일을 사용하기 위해 검증하고, 기본값으로 초기화하는 과정
- Initialization : static field의 값들을 정의한 값으로 초기화하는 과정
이 클래스로더는 컴파일 단계가 아닌 런타임 단계에서 클래스를 로딩할 수 있게 해준다는 의미도 된다. 그렇기 때문에 로드타임 동적 로딩(LoadTime Dynamic Loading)과 런타임 동적 로딩(RunTime Dynamic Loading)을 할 수 있다.
- 로드타임 동적 로딩 : 특정 클래스를 로딩하는 과정에서 필요한 클래스를 로딩한다
- 즉, 하나의 클래스를 로딩하는 과정에서 동적으로 필요한 클래스를 로딩하는 것
- 런타임 동적 로딩 : 클래스를 로딩할 때가 아닌, 코드를 실행하는 순간에 클래스를 로딩하는 것
요약하자면?
클래스로더는 런타임동안 Java 클래스를 JVM에 동적으로 로드하는 역할을 한다. 따라서 JVM은 클래스로더 덕분에 Java 애플리케이션을 실행하기 위해 기본 파일이나 파일 시스템에 대해 알 필요가 없는 것이다. 또한 Java 클래스는 한 번에 메모리에 로드되지 않고 애플리케이션에서 필요할 때 로드하게 된다.
📄 클래스로더의 특징
1. 계층 (Hierarchical) 구조
클래스로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다.
2. 위임 원칙 (Delegation Principle)
계층 구조를 바탕으로 클래스로더끼리 로드를 위임하는 구조로 동작한다.
클래스를 로드할 때 먼저 상위 클래스로더를 확인하여 있다면 사용하고 없다면 하위 클래스로더로 찾아서 로드한다.
3. 가시범위 원칙 (Visibility Principle)
하위 클래스로더는 상위 클래스로더를 찾을 수 있지만, 상위 클래스로더는 하위 클래스로더의 클래스를 찾을 수 없다.
4. 언로드 불가 (Cannot Unload Classes)
클래스로더는 클래스를 로드할 수 있지만 언로드할 수는 없다.
언로드 대신, 현재 클래스로더를 삭제하고 새로운 클래스로더를 생성하는 방법을 사용할 수 있다.
각 클래스로더는 로드된 클래스를 보관하는 네임스페이스(namespace)를 갖는다.
클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN을 기준으로 클래스를 찾는다.
비록 FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스로더가 로드한 클래스이면 다른 클래스로 간주된다.
5. 유일성 원칙 (Uniqueness Principle)
하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 다시 로딩하지 않게 해서 로딩된 클래스의 유일성을 보장한다.
유일성을 식별하는 기준은 클래스의 binary name이다.
💻 클래스로더 - Loading
.class 파일을 읽고, 바이너리 데이터로 만들어서 Method Area에 저장하는 과정이다.
저장되는 데이터는 다음과 같다.
- Class, Interface, Enum
- 메소드와 변수
- FQCN (Fully Qualified Class Name) : 클래스가 속한 패키지명을 모두 포함한 이름
로딩이 끝나면 해당 class 타입의 객체를 생성하여 Heap Area에 저장한다.
위 내용처럼 Loading은 클래스로더가 필요한 클래스 파일들을 찾아 탑재하게 된다.
각각의 클래스 파일들이 기본으로 제공받는 클래스 파일인지 혹은 개발자가 정의한 클래스 파일인지와 같은 기준에 의해서 클래스로더의 수준도 3가지로 나뉘게 된다.
1) Bootstrap ClassLoader
- 다른 모든 클래스로더의 부모가 되는 클래스로더
- jre/lib/rt.jar를 포함하여 JVM을 구동시키기 위한 가장 필수적인 라이브러리의 클래스들을 JVM에 탑재
- 가장 상위 클래스로더이므로 다른 클래스로더와는 다르게 탑재되는 OS에 맞게 Native Code로 구현되어 있음
Native Code : C, C++처럼 인터프리터 없이 운영체제가 읽을 수 있는 형태로 컴파일한 사용 가능한 코드
2) Extension ClassLoader
- Bootstrap ClassLoader 다음으로 우선순위를 가지는 클래스로더
- Java 9부터는 Platform ClassLoader로 불림
- localedata, zipfs 등 다른 표준 핵심 Java Class의 라이브러리들을 JVM에 탑재하는 역할을 하고 있음
- 이들은 $JAVAHOME/jre/lib/ext_ 에 있음
- Java로 구현되어 있고, sun.misc.Launcher 클래스 안에 static 클래스로 구현되어 있고, URLClassLoader를 상속하고 있음
3) Application ClassLoader
- Java 9부터는 System ClassLoader 라고 불림
- Classpath에 있는 클래스들을 탑재
- 개발자들이 자바 코드로 작성한 클래스 파일들을 JVM에 탑재하는 역할을 하고 있음
- 만약 개발자가 클래스로더를 구현하여 사용하게 되면 기본적으로 Application ClassLoader의 자식 형태로 사용자 정의된 클래스로더를 구성하게 된다. 즉, 개발자가 작성한 클래스는 Application ClassLoader가 로드한다.
- 이것 역시 Extension ClassLoader와 마찬가지로 Java로 구현되어 있고, sun.misc.Launcher 클래스 안에 static 클래스로 구현되어 있으며, URLClassLoader를 상속하고 있음
간단하게 요약하자면
Bootstrap ClassLoader와 Extension ClassLoader는 JVM 자체의 구성 요소들을 로드하는 것
Application ClassLoader는 개발자가 작성한 클래스들을 로드하는 것
다시 위임원칙에 대해 자세히 살펴보자
위임원칙은 클래스 로딩이 필요할 때 3가지 기본 클래스로더의 윗방향으로 클래스 로딩을 위임하는 것이다.
main() 메소드가 포함된 ClassLoaderRunner 클래스에서 개발자가 직접 작성한 Internal 클래스를 로딩하는 과정을 살펴보자.
Oracle Java 매거진과 다른 곳에서는 먼저 클래스가 로드되었는지 여부를 확인한다고 합니다. 이러면 이중 작업을 수행하고 클래스를 반복적으로 로드하는 것을 방지할 수 있습니다. 이렇게 여부를 확인했는데 JVM 메모리 영역(Method Area)에 클래스가 없는 경우 아래 과정을 거치게 됩니다.
위 과정은 JVM 메모리(Method Area) 영역에 사용자 정의 클래스인 Internal 클래스가 없다고 가정한 상태입니다.
- ClassLoaderRunner는 자기 자신을 로딩한 Application ClassLoader에게 Internal 클래스 로딩을 요청
- 클래스 로딩 요청을 받은 Application ClassLoder는 Internal을 직접 로딩하지 않고 상위 클래스로더에 위임
- 클래스 로딩 요청을 받은 Extension ClassLoader도 직접 로딩하지 않고 상위 클래스로더에게 위임
- Bootstrap ClassLoader는 rt.jar에서 Internal 클래스를 찾기 시도
- 있으면 로딩 후 반환
- 없으면 Extension ClassLodaer가 jre/lib/ext 폴더나 java.ext.dirs 환경 변수로 지정된 폴더에서 찾기 시도
- 있으면 로딩 후 반환
- 없으면 Application ClassLoader가 Class Path에서 Internal 클래스를 찾기 시도
- 있으면 로딩 후 반환
- 없으면 ClassNotFoundException이 발생
이런 식으로 동작하는 이유는 특징에서 말했던 3가지 원칙과 관련이 있다.
바로 가시성, 유일성, 위임 원칙이다.
Application ClassLoader에서 살펴봤지만 개발자가 작성한 클래스를 로딩하는 Application ClassLoader가 Bootstrap ClassLoader에 의해 로딩된 String.class 를 볼 수 없다면 Application ClassLoader는 String.class를 사용할 수 없다.
따라서 하위에서는 상위를 볼 수 있어야 제대로 동작하게 된다.
거기다가 상위 클래스로더가 로딩한 클래스를 다시 로딩하지 않는 유일성을 보장하므로 상위에서 찾는것이 효율적이게 된다.
이렇게 JVM에 탑재된 클래스 파일은 종료될 때까지 JVM에서 제거되지 않는다.
🤔 그렇다면 ClassLoader의 실제 구현 구조는 어떻게 되어있을까?
위임원칙으로 클래스로더는 순서대로 클래스 파일을 찾게된다.
실제로 코드를 통해 부모 역할을 하는 클래스로더를 찾으면 다음과 같이 나온다.
이렇게 3가지 클래스로더를 볼 수 있는데, 참고로 getParent()는 부모클래스가 아닌 상위 파일을 찾는 메소드이다.
UserClass는 개발자가 작성한 클래스이다.
Extension ClassLoader에 나오는건 $JAVAHOME/jre/lib/ext_ 에 있는 클래스이고,
Bootstrap ClassLoader는 rt.jar에 담긴 클래스이다.
그런데 어째서 Bootstrap ClassLoader는 null 값을 반환하는 걸까?
이것은 Native하게 코드가 작성되어있기에 null 값을 반환하는 것이다.
나머지 클래스로더는 URLClassLoader, SecureClassLoader라는 객체를 상속받아서 구현된다.
그래서 구현은 다음과 같이 이루어지게 된다.
📄 Java 8 과 Java 9부터의 차이점
💻 클래스로더 - Linking
Linking은 로드된 클래스 파일들을 검증하고, 사용할 수 있게 준비하는 과정이다.
Verification(검증), Preparation(준비), Resolution(분석) 이라는 세 단계로 이루어져 있다.
1) 검증
클래스 파일이 유효한지 확인하는 과정이다.
클래스 파일이 JVM 구동 조건대로 구현되지 않았을 경우 VerifyError를 던진다.
클래스로더의 모든 과정 중에서 가장 까다로운 검사를 수행하는 과정이므로 가장 복잡하고 시간이 많이 걸린다.
2) 준비
클래스 및 인터페이스에 필요한 static field 메모리를 할당하고, 이를 기본값으로 초기화한다.
기본값으로 초기화된 static field 값들은 Initialization 과정에서 코드에 작성한 초기값으로 변견된다.
이 때문에 JVM에 탑재된 클래스 파일의 코드를 작동시키지는 않는다.
3) 분석
Symbolic Reference 값을 JVM의 메모리 구성 요소인 Method Area의 런타임 환경 풀을 통하여 Direct Reference라는 메모리 주소 값으로 바꾸어준다. 해당 단계의 영향을 받는 JVM Instruction 요소는 new 및 instanceof가 있다.
* Symbolic Reference : 특정 메모리 주소를 참조한 것이 아닌, 참조하는 대상의 이름만 지칭한 것
* Direct Reference : 참조하는 클래스의 특정 메모리 주소를 참조하는 것
💻 클래스로더 - Initializaiton
Linking 과정을 거치고 Initialization 단계에서는 클래스 파일의 코드를 읽게 된다.
자바 코드에서의 class와 interface의 값들을 지정한 값들로 초기화 및 초기화 메소드를 실행시킨다.
이 때, JVM은 멀티 쓰레딩으로 작동하며, 같은 시간에 한 번에 초기화를 하는 경우가 있기 때문에 초기화 단계에서도 동시성을 고려해야한다.
클래스로더를 통한 클래스 탑재 과정이 끝나면 본격적으로 JVM에서 클래스 파일을 구동시킬 준비가 끝난다.
🖥 마무리 정리
클래스로더는 바이트코드(.class)를 Runtime Data Area에 탑재한다.
그 때 위 이미지처럼 진행이 된다.
- Loading
- 클래스로더가 클래스 파일을 읽고 바이너리 데이터를 생성하여 JVM 메모리의 Method Area에 저장
- 로딩 완료 시 해당 클래스 타입의 메타데이터 객체(Class<?>)를 생성해서 Heap Area에 저장
- Linking
- 클래스 파일을 사용하기 위해 필요한 메모리 할당 및 연결하고, 검증하고, 기본값으로 초기화하는 과정
- 검증 : 로드한 클래스 파일이 자바 언어 명세 및 JVM 명세에 명시된 대로 구성되었는지 검증
- 분석 : Symbolic Reference를 Direct Reference로 교체
- Initialization
- static field의 값들을 정의한 값으로 초기화하는 과정
참고
https://www.baeldung.com/java-classloaders
https://tecoble.techcourse.co.kr/post/2021-07-15-jvm-classloader/
https://d2.naver.com/helloworld/1230
'프로그래밍 > Java' 카테고리의 다른 글
List 인터페이스 (0) | 2022.02.22 |
---|---|
Java 줄바꿈문자 \n 이란? (0) | 2022.02.17 |
자바 getBytes() (0) | 2022.01.28 |
자바 toCharArray() (2) | 2022.01.28 |
JVM(자바가상머신)이란? - Part 2, Execution Engine (0) | 2022.01.26 |