GC에 대해 공부를 하다가 문득 이런 생각이 들었다. 나란 남자 JVM은 빠삭하게 이해하고 있는게 맞는걸까?
누군가 JVM의 아키텍쳐와 메커니즘에 대해 상세하게 설명해달라고 물어보면 눈동자가 흔들리고 자리를 피할 것이다.
그래서 JVM에 대해 좀 딥하게 파보기로 했다.
JVM 이란?
JVM은 java virtual machine으로, 말 그대로 자바가상머신이다.
JVM은 자바 응용프로그램으로부터 전달받은 명령을 해당 운영체제가 이해할 수 있도록 변환하여 전달하는 역할을 하며 WORA(Write Once Run Anywhere) 한번 쓰면 VM을 통해 어디서나 실행가능한 개념으로 개발되었다.
JVM의 역할을 크게 보면 아래와 같다.
- Bytecode 로드 및 해석
- 안전성 확보
- 자동 메모리 관리
그렇다면 JVM의 구조는 어떻게 이뤄져 있을까? 한번 알아보자.
위의 아키텍쳐와 같이 JVM은 기본 3개의 시스템으로 나뉜다.
- 클래스 로더 서브시스템 (Class Loader SubSystem)
- 런타임 데이터 영역 (Runtime Data Areas)
- 실행 엔진 (Execution Engine)
하나의 글에 이 내용을 다 담으려니 너무 길어지고 가독성이 떨어져 하나하나 딥하게 파보자.
그래서 오늘은 클래스 로더 서브시스템(Class Loader SubSystem)에 대해 분석해보려고 한다.
- 클래스 로더 서브시스템(Class Loader SubSystem)
Java의 동적 클래스 로딩 기능은 클래스 로더 서브시스템에서 처리된다. 런타임에 처음으로 클래스를 참조할 때 클래스 파일을 초기화하고 로드한다. |
이게 무슨 소린지 하나하나 파헤쳐 보자.
클래스 로더 서브시스템에서도 3개의 프로세스로 이루어져 있다.
1. 로딩(Loading)
2. 연결(Linking)
3. 초기화 (Initialize)
1. 로딩 Loading
로딩은 하드 디스크에서 .class 파일을 읽고 이진 데이터를 JVM의 메서드 영역 내부에 저장하는 것을 의미한다. Bootstrap, Extension, application 이 세개의 클래스 로더를 통해 클래스 파일이 로드되며, 이 세개의 클래스로더는 서로 상속관계로 정의되어 있고 위임을 하는 방식으로 작업을 진행한다.
- Bootstrap Class Loader
Bootstrap Class Loader는 최상위 클래스 로더이며 Native C로 구현돼 있어서, String.class.getClassLoader()는 그냥 null을 반환한다. Primordial ClassLoader 라고 불리기도 한다. jre의 lib폴더에 있는 rt.jar 파일을 뒤져 기본 자바 API 라이브러리를 로드한다. 여기서 rt.jar는 runtime java를 나타내며, jvm이 돌아가기 위해 기본적인 Java API를 담고 있는 class archive라고 볼 수 있다.
- Extension Class Loader
- Bootstrap Class Loader의 자식 관계이며, JDK 확장 라이브러리에서 핵심 자바 클래스를 로드하는데, jre/lib/ext 폴더나 java.ext.dirs 환경변수로 지정된 폴더의 클래스 파일을 로드한다. 해당 경로에 개발자가 직접 jar파일을 추가해 줄 수도 있다.
- Application Class Loader
- Application Class Loader는 -classpath(또는 -cp)나 JAR 파일 안에 있는 Manifest 파일의 Class-Path 속성값으로 지정된 폴더에 있는 클래스를 로딩한다.
개발자가 애플리케이션 구동을 위해 직접 작성한 대부분의 클래스는 이 애플리케이션 클래스로더에 의해 로딩된다.
*manifest파일이 뭔데..?
- manifest 파일은 JAR 파일에 패키징된 파일들에 대한 메타데이터성 정보를 가지고 있는 파일
아래의 사진을 통해 각각의 클래스가 어떤 클래스로더에서 로드되는지 확인할 수 있다.
이러한 3개의 클래스 로더에는 3가지의 작동원칙이 있다. 고슬링형님은 3을 참 좋아하는 것 같다,,
1. 위임원칙(Delegation Principle)
위임 원칙은 클래스 로딩이 필요할 때 3가지 기본 클래스로더의 윗 방향으로 클래스 로딩을 위임하는 것을 말한다. main() 메서드가 포함된 ClassLoaderRunner 클래스에서 개발자가 직접 작성한 Internal 클래스를 로딩하는 과정일을 그림으로 표현하면 다음과 같다.
*위의 프로세스가 실행되기전의 시나리오
JVM이 클래스를 통해 올 때 마다 해당 클래스가 이미 로드되었는지 여부를 확인한다. 만약 클래스가 메서드 영역에 이미 로드된 경우 JVM은 실행으로 진행된다. 메서드 영역에 클래스가 없는 경우 JVM은 Class Loader SubSystem에 해당 클래스를 로드하도록 요청한다. 이 요청에서부터 위의 프로세스가 시작된다.
- ClassLoaderRunner는 자기 자신을 로딩한 Application Class Loader에게 Internal 클래스 로딩을 요청한다.
- 클래스 로딩 요청을 받은 Application Class Loader는 Internal을 스스로 직접 로딩하지 않고 상위 클래스로더인 Extension Class Loader에게 위임한다.
- 클래스 로딩 요청을 받은 Extension Class Loader도 Internal을 스스로 직접 로딩하지 않고 상위 클래스로더인 Bootstrap Class Loader에게 위임한다.
- Bootstrap Class Loader는 rt.jar에서 Internal을 찾아서 있으면 로딩 후 반환하고
- 없으면 Extension Class Loader가 jre/lib/ext 폴더나 java.ext.dirs 환경 변수로 지정된 폴더에서 Internal을 찾아서 있으면 로딩 후 반환하고
- 없으면 Application Class Loader가 클래스패스에서 Internal을 찾아서 있으면 로딩 후 반환하고 최하위 레벨인 Application Class Loader에서도 없으면 ClassNotFoundException이 발생한다.
2. 가시범위 원칙(Visibility Principle)
가시범위 원칙은 하위 클래스로더는 상위클래스로더가 로딩한 클래스를 볼 수 있지만, 상위 클래스로더는 하위 클래스 로더가 로딩한 클래스를 볼 수 없다는 원칙이다. 만약 Application Class Loader가 Bootstrap Class Loader에 의해 로딩된 String.class를 볼 수 없다면 애플리케이션은 String.class를 사용할 수 없을 것이다.
또한 상위클래스로더에서 하위 클래스로더를 볼 수 있다면, 상/하관계로 나눈 클래스로더의 의미와 프로세스의 가치에 대해 의문이 생길 것이다.
3. 유일성 원칙(Uniqueness Principle)
유일성 원칙은 하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 다시 로딩하지 않게 해서 로딩된 클래스의 유일성을 보장하는 것이다. Binary name을 기준으로 클래스의 유일성을 식별한다. 즉 중복로딩을 막는 작업이다.
따라서 class파일을 로드한 후 JVM은 힙 메모리에 로드된 class에 대한 개체를 만든다.
JVM에서는 프로그램에서 같은 클래스를 여러번 사용하여도 하나의 class 개체만 생성된다.
2. 연결(Linking)
클래스 로더 서브시스템에서는 또 또 또 3개의 연결과정을 거친다
- 검증 (verify)
검증단계에서 JVM의 Byte Code Verifier(검증자)는
class파일이 유효한 컴파일러에 의해 생성되었는지 class파일이 올바른 형식화가 되어 있는지 클래스 또는 인터페이스의 바이트코드가 올바른지 |
검증하고 올바르지 않다면 java.lang.VerifyError를 발생시킨다.
*이러한 검증작업때문에 Java가 보호 언어라고 할 수 있다. 왜냐하면 공격자가 class파일에 바이러스를 만들기 위해 수동으로 변경하면 Bytecode 검사기는 유효한 컴파일러에 의해 생성되지 않으므로 해당 클래스 파일을 감지하며 검증 실패를 하게 된다.
- 준비 (prepare)
이 단계에서 JVM은 클래스 레벨 또는 인터페이스 수준 static 변수에 대한 메모리를 할당하고 기본 값을 할당함.
Ex ) boolean -> false
여기서 메모리를 할당하기에 충분하지 않은 경우 OutOfMemoryError가 throw된다.
- 해석 (resolve)
이 과정은 Symbolic reference (다른 클래스를 가리키는 논리 참조)가 있는 경우 reference class를 메모리에 로드하고 실제 메모리 위치를 사용하여 symbolic reference를 교체한다. 한 개의 symbolic reference가 클래스에서 여러 번 사용되는 경우 교체는 한 번만 수행된다. 즉, 참조하고 있는 클래스의 껍데기(Symbolic reference)를 실제 참조 클래스의 메모리와 교체하는 작업을 한다.
3. 초기화 (Initialize Stage)
초기화 단계에서는 모든 static변수가 실제 값으로 할당되고, static 블록은 부모에서 자식으로, 위에서 아래로 실행된다.
정리
정리하면, 클래스 로더 서브시스템은 로딩 - 연결 - 초기화 3개의 프로세스를 가진다.
로딩단계에서는, 3개의 클래스로더를 통해 class파일을 읽어들이고 class에 대한 정보를 반환한다.
이어서 연결단계에서 해당 class파일에 대해 올바른지 검증을 한 후, static 변수에 대한 메모리를 할당하고 default값을 할당한다.
마지막으로 초기화 단계에서, 모든 static 변수가 실제 값으로 할당되고, static 블록이 실행되어 진다.
클래스 로더 서브 시스템의 프로세스에 관해서는 여기까지 알아보고, 추후 좀 더 딥하게 풀어낼 수 있는 정보나 조금 더 쉬운 방식의 설명이 있다면 조금 씩 수정하도록 해야겠다.
참고
Java 클래스로더 훑어보기 - 뒤태지존의 끄적거림 (homoefficio.github.io)
자바의 클래스 로더 - 괴짜 (geeksforgeeks.org)
JVM 메모리 구조 및 아키텍쳐(클래스 로더, 실행엔진 등) (tistory.com)
클래스 로더 서브 시스템 | JVM 내부 | 코드 호박 (codepumpkin.com)
JVM 아키텍처: JVM 클래스 로더 및 런타임 데이터 영역 - 자바 코드 괴짜 - 2021 (javacodegeeks.com)
'Language > ☕️Java' 카테고리의 다른 글
StringBuffer에서 Thread safe의 원리 및 Example Sample Code를 통한 고찰 (0) | 2022.08.16 |
---|---|
JAVA로 카카오 메시지 API연동 (4) | 2022.02.25 |
Java Collections 시간 복잡도 (0) | 2021.12.03 |
Java 직렬화란(Serialization)? (0) | 2021.08.19 |
class파일로 컴파일 후, FTP를 통한 배포에서 발생한 문제. (UnsupportedClassVersionError ) (0) | 2021.08.14 |
댓글