JVM은 클래스를 설명하는 데이터를 클래스 파일로부터 메모리로 읽어 들이고 그 데이터를 검증, 변환, 초기화하고 나서 최종적으로 가상 머신이 곧바로 사용할 수 있는 자바 타입을 생성한다. 이 과정을 가상 머신의 클래스 로딩 메커니즘이라고 한다.
컴파일 시 링크까지 해야 하는 언어들과 달리 자바 언어에서는 클래스 로딩, 링킹, 초기화가 모두 실행 중에 이루어진다. 그래서 자바 언어는 AOT 컴파일에 제약이 생기고 클래스 로딩을 거치느라 실행 성능이 살짝 떨어진다. 하지만 이는 자바 애플리케이션의 높은 확장성과 유연성을 가능케 하는 이점으로도 작용한다.
JVM의 메모리에 로드되는 걸 시작으로 다시 언로드될 때까지 로딩 -> 검증 -> 준비 -> 해석 -> 초기화 -> 사용 -> 언로딩
과정을 거친다. 이 중 검증, 준비, 해석
단계를 묶어 링킹이라고 한다.
로딩, 검증, 준비, 초기화, 언로딩은 반드시 순서대로 진행해야 한다. 반면 해석 단계는 그렇지 않아서 때에 따라서는 초기화 후에 시작할 수 있다. 이는 자바 언어의 런타임 바인딩을 지원하기 위해서다.
자바 가상 머신 명세는 클래스 로딩 과정의 첫 단계인 '로딩'을 정확히 어떤 상황에서 시작해야 하는지 명시하지 않았다. 따라서 가상 머신 구현자가 자유롭게 선택할 수 있다. 반면 초기화 단계는 즉시 시작되어야 하는 상황 여섯 가지를 엄격히 규정했다.
1. 바이트코드 명령어인 new, getstatic, putstatic, invokestatic
을 만났을 때 해당 타입이 아직 초기화되어 있지 않다면 초기화를 촉발한다.
REF_getStatic, REF_putStatic, REF_invoiceStatic, REF_newInvokeSpecial
타입 메서드 핸들을 해석해 얻은 java.lang.invoke.MethodHandle
인스턴스를 호출할 때 해당하는 클래스가 초기화되어 있지 않았다면 초기화를 촉발한다.로딩 -> 검증 -> 준비 -> 해석 -> 초기화
5 단계 각각에서 이루어지는 일을 알아보자
java.lang.Class
객체를 힙 메모리에 생성한다로딩 단계는 자바 가상 머신에 내장된 부트스트랩 클래스 로더를 사용하거나 사용자 정의 클래스 로더를 사용하여 수행할 수 있다. ClassLoader
의 findClass() or loadClass()
메서드를 오버라이딩하면 바이트 스트림을 얻는 방법을 통제할 수 있다.
배열 클래스는 클래스 로더가 생성하지 않고 자바 가상 머신이 직접 메모리에 동적으로 생성한다. 배열 클래스의 원소 타입은 클래스 로더를 통해 로드된다.
검증 단계는 매우 중요하다. 엄격하게 진행해야 자바 가상 머신이 악성 코드로부터 자신을 보호할 수 있다. 그래서 코드의 양적 측면과 실행 성능 비용 측면 모두에서 검증 단계는 클랫 ㅡ로딩 과정 중 상당한 비중을 차지한다. 검증은 크게 4단계를 거쳐 완료된다.
바이트 스트림이 파일 형식에 부합하고 현재 버전의 가상 머신에서 처리될 수 있는지 확인.
바이트 코드로 설명된 정보의 의미를 분석하여 서술된 정보가 요구 사항을 충족하는지 확인한다.
전체 검증 과정에서 가장 복잡한 단계다. 주된 목적은 데이터 흐름과 제어 흐름을 분석하여 프로그램의 의미가 적법하고 논리적인지 확인하는 것 이다. 검증을 통과했다고 해서 100% 안전하다는 보장은 없다. 최대한 많은 사항을 철저히 검사하더라도 한계는 있다.
마지막 단계는 가상 머신이 심벌 참조를 직접 참조로 변환할 때 수행된다.
이 변환은 링킹의 세 번째 단계인 해석 단계에서 일어난다. 심벌 참조 검증은 해당 클래스 자체를 제외한 모든 정보를 확인하는 것으로 보면 된다. 쉽게 이야기하면 현재 클래스가 참조한느 특정 외부 클래스, 메서드, 필드, 그 외 자원들에 접근할 권한이 있는지 본다.
운영 환경에서 실행할 때는 검증을 건너뛰기도 한다. 오래 기간 반복 사용해 왔다면 신뢰할 수 있다는 것이고 검증을 생략하면 가상 머신이 클래스 로딩하는 시간이 제법 단축된다.
준비는 클래스 변수를 메모리에 할당하고 초깃값을 설정하는 단계다.
인스턴스 변수가 아닌 클래스 변수만 할당된다. 인스턴스 변수는 객체가 인스턴스화될 때 객체와 함께 자바 힙에 할당된다.
준비 단계에서 클래스 변수에 할당하는 초깃값은 해당 데이터 타입의 제로 값이다.
Public static int value = 123;
준비 단계를 마친 직후 value 값은 0이다.
123을 할당하는 단계는 '클래스 초기화 단계' 이다.
public static final int value = 123;
final을 통해 ConstantValue 속성이 생성되면 준비단계에 바로 123을 할당한다.
해석은 자바 가상 머신이 상수 풀의 심벌 참조를 직접 참조로 대체하는 과정이다.
심벌 참조는 가상 머신이 구현한 메모리 레이아웃과는 아무런 관련이 없다. 참조 대상이 반드시 가상 머신의 메모리에 로드되어 있을 필요도 없다. 메모리 레이아웃은 가상 머신 구현에 따라 달라질 수 있지만 심벌 참조는 달라지지 않는다.
직접 참조는 포인터, 상대적 위치(오프셋) 또는 대상의 위치를 간접적으로 가리키는 핸들이다. 가상 머신에 구현된 메모리 레이아웃과 밀접하게 관련된다. 똑같은 심벌 참조로부터 변환했더라도 직접 참조는 가상 머신에 따라 달라지는 게 보통이다.
초기화는 클래스 로딩의 마지막 단계다.
초기화 단계에 들어서면 JVM이 드디어 사용자 클래스에 작성된 자바 프로그램 코드를 실행하기 시작한다. 앞서 준비 단계에서는 모든 변수에 시스템이 정의한 초깃값인 0을 할당했다. 반면 초기화 단계에서는 클래스 변수와 기타 자원을 개발자가 프로그램 코드에 기술한 대로 초기화한다.
클래스 로더는 당연하게도 클래스를 로딩하는 일을 하지만 그 일이 전부는 아니다.
클래스 로더를 빼놓고는 특정 클래스가 JVM에서 유일한지 판단할 수 없다. 같은 클래스를 다른 클래스 로더로 읽는다면 다른 클래스로 인식된다.
JVM 관점에서 클래스 로더의 종류는 2 가지뿐이다.
java.lang.ClassLoader
를 상속하여 자바로 구현하며, JVM 외부에 독립적으로 존재JDK 8까지 유지된 3계층 클래스 로더와 부모 위임 모델이 무엇인지 알아보겠다.
JAVA_HOME/lib/ or -Xbootclasspath
매개 변수로 지정한 경로에 위치한 파일들과 JVM이 클래스 라이브러리로 인식하는 파일들을 로드한느 일을 책임sun.misc.Launcher$ExtClassLoader
를 말하며 JAVA_HOME/lib/ext or java.ext.dirs
시스템 변수로 지정하는 경로의 클래스 라이브러리들을 로드하는 역할 sun.misc.Launcher$AppClassLoader
를 말한다. 클래스패스상의 클래스 라이브러리들을 로드하는 역할사용자 정의 클래스 로더 -> 애플리케이션 클래스 로더 -> 확장 클래스 로더 -> 부트스트랩 클래스 로더
로 계층 관계를 클래스 로더들의 부모 위임 모델이라고 한다.
클래스 로딩을 요청받은 클래스 로더는 처음부터 클래스 자체를 로드하려 시도하지 않는다. 그 대신 수준에 맞는 상위 클래스 로더로 요청을 위임한다. 따라서 부트스트랩 클래스 로더
로 넘겨지며 자신이 처리할 요청이 아니라고 판단되면 하위 로더로 넘겨진다.
동일한 클래스에 대해 클래스 로더가 동작함으로 동일한 class의 생성을 보장한다.
JDK 9부터는 Jigsaw 클래스 로더가 사용된다고 한다.
Jigsaw 프로젝트의 결과로 JDK 9에 도입된 모듈 시스템(JPMS)은 자바 기술에 있어서 중요한 개선이다. JDK 9의 모듈은 JAR 패키지 같은 단순한 코드 컨테이너가 아니다. 자바 모듈 정의에는 코드 외에도 다음 내용이 포함된다.
자바 모듈 시스템이 기존 클래스패스 방식과 호환되도록 하기 위해 JDK 9에서는 클래스패스에 해당하는 모듈패스 개념을 도입했다.
자바 설계진은 클래스패스 방식의 기존 자바 애플리케이션도 아무런 수정 없이 JDK 9 이상의 최신 환경에서 실행할 수 있도록 다음 규칙을 적용했다.
JDK 9도 하위 호환을 위해 3계층 클래스 로더 아키텍처와 부모 위임 모델의 근간을 흔들지는 않았다. 주목할 만한 변화는 다음과 같다.
java.net.URLClassLoader
로부터 파생되지 않는다.jdk.internal.loader.BuiltinClassLoader
에서 파생된다.