JVM 클래스 로딩 메커니즘

S_H_H·2025년 3월 28일
0
post-thumbnail

JVM은 클래스를 설명하는 데이터를 클래스 파일로부터 메모리로 읽어 들이고 그 데이터를 검증, 변환, 초기화하고 나서 최종적으로 가상 머신이 곧바로 사용할 수 있는 자바 타입을 생성한다. 이 과정을 가상 머신의 클래스 로딩 메커니즘이라고 한다.

컴파일 시 링크까지 해야 하는 언어들과 달리 자바 언어에서는 클래스 로딩, 링킹, 초기화가 모두 실행 중에 이루어진다. 그래서 자바 언어는 AOT 컴파일에 제약이 생기고 클래스 로딩을 거치느라 실행 성능이 살짝 떨어진다. 하지만 이는 자바 애플리케이션의 높은 확장성과 유연성을 가능케 하는 이점으로도 작용한다.

클래스 로딩 시점

JVM의 메모리에 로드되는 걸 시작으로 다시 언로드될 때까지 로딩 -> 검증 -> 준비 -> 해석 -> 초기화 -> 사용 -> 언로딩 과정을 거친다. 이 중 검증, 준비, 해석 단계를 묶어 링킹이라고 한다.
로딩, 검증, 준비, 초기화, 언로딩은 반드시 순서대로 진행해야 한다. 반면 해석 단계는 그렇지 않아서 때에 따라서는 초기화 후에 시작할 수 있다. 이는 자바 언어의 런타임 바인딩을 지원하기 위해서다.

자바 가상 머신 명세는 클래스 로딩 과정의 첫 단계인 '로딩'을 정확히 어떤 상황에서 시작해야 하는지 명시하지 않았다. 따라서 가상 머신 구현자가 자유롭게 선택할 수 있다. 반면 초기화 단계는 즉시 시작되어야 하는 상황 여섯 가지를 엄격히 규정했다.
1. 바이트코드 명령어인 new, getstatic, putstatic, invokestatic을 만났을 때 해당 타입이 아직 초기화되어 있지 않다면 초기화를 촉발한다.

  • new 키워드로 객체의 인스턴스 생성
  • 타입의 정적 필드를 읽거나 설정
  • 타입의 정적 메서드 호출
  1. class 클래스나 java.lang.reflect 패키지 등 표준 클래스 라이브러리에서 제공하는 리플렉션 메서드를 사용할 때 해당 타입이 아직 초기화되어 있지 않다면 초기화를 촉발한다.
  2. 클래스를 초기화할 때 상위 클래스가 초기화되어 있지 않다면 상위 클래스 초기화를 촉발한다.
  3. 가상 머신은 구동 직후 사용자가 지정한 메인 타입을 찾아 실행한다. 이 때 메인 타입의 초기화를 먼저 시작한다.
  4. REF_getStatic, REF_putStatic, REF_invoiceStatic, REF_newInvokeSpecial 타입 메서드 핸들을 해석해 얻은 java.lang.invoke.MethodHandle 인스턴스를 호출할 때 해당하는 클래스가 초기화되어 있지 않았다면 초기화를 촉발한다.
  5. 인터페이스에 디폴트 메서드를 정의했다면, 해당 인터페이스를 직간접적으로 구현한 클래스가 초기화 될 때 인터페이스부터 초기화한다.

클래스 로딩 처리 과정

로딩 -> 검증 -> 준비 -> 해석 -> 초기화 5 단계 각각에서 이루어지는 일을 알아보자

로딩

  1. 완전한 이름을 보고 해당 클래스를 정의하는 바이너리 바이트 스트림을 가져온다.
  2. 바이트 스트림으로 표현된 정적인 저장 구조를 메서드 영역에서 사용하는 런타임 데이터 구조로 변환한다.
  3. 로딩 대상 클래슬르 표현하는 java.lang.Class 객체를 힙 메모리에 생성한다

로딩 단계는 자바 가상 머신에 내장된 부트스트랩 클래스 로더를 사용하거나 사용자 정의 클래스 로더를 사용하여 수행할 수 있다. ClassLoaderfindClass() or loadClass() 메서드를 오버라이딩하면 바이트 스트림을 얻는 방법을 통제할 수 있다.

배열 클래스는 클래스 로더가 생성하지 않고 자바 가상 머신이 직접 메모리에 동적으로 생성한다. 배열 클래스의 원소 타입은 클래스 로더를 통해 로드된다.

검증

  1. 클래스 파일의 바이트 스트림에 담긴 정보가 규정한 모든 제약을 만족하는지 확인
  2. 이 정보를 코드로 변환해 실행했을 때 자바 가상 머신 자체의 보안을 위협하지 않는지 확인

검증 단계는 매우 중요하다. 엄격하게 진행해야 자바 가상 머신이 악성 코드로부터 자신을 보호할 수 있다. 그래서 코드의 양적 측면과 실행 성능 비용 측면 모두에서 검증 단계는 클랫 ㅡ로딩 과정 중 상당한 비중을 차지한다. 검증은 크게 4단계를 거쳐 완료된다.

파일 형식 검증

바이트 스트림이 파일 형식에 부합하고 현재 버전의 가상 머신에서 처리될 수 있는지 확인.

  • 매직 넘버 확인
  • 메이저 버전과 마이너 버전 번호가 허용하는 범위 확인
  • 상수 풀에 지원하지 않는 상수가 들어 있는지
  • 인덱스 값 중 상수나 타입에 맞지 않는 상수를 가리키는 경우
  • UTF-8 인코등에 부합하지 않는 데이터 확인
  • 생략되었거나 추가된 정보가 있는지
  • ...

메타 데이터 검증

바이트 코드로 설명된 정보의 의미를 분석하여 서술된 정보가 요구 사항을 충족하는지 확인한다.

  • 상위 클래스가 있는지
  • 상위 클래스가 상속을 허용하는지
  • 상위 클래스 또는 인터페이스에서 정의한 필수 메서드를 모두 구현했는지
  • 필드와 메서드가 상위 클래스와 충돌하는지
  • ...

바이트코드 검증

전체 검증 과정에서 가장 복잡한 단계다. 주된 목적은 데이터 흐름과 제어 흐름을 분석하여 프로그램의 의미가 적법하고 논리적인지 확인하는 것 이다. 검증을 통과했다고 해서 100% 안전하다는 보장은 없다. 최대한 많은 사항을 철저히 검사하더라도 한계는 있다.

심벌 참조 검증

마지막 단계는 가상 머신이 심벌 참조를 직접 참조로 변환할 때 수행된다.
이 변환은 링킹의 세 번째 단계인 해석 단계에서 일어난다. 심벌 참조 검증은 해당 클래스 자체를 제외한 모든 정보를 확인하는 것으로 보면 된다. 쉽게 이야기하면 현재 클래스가 참조한느 특정 외부 클래스, 메서드, 필드, 그 외 자원들에 접근할 권한이 있는지 본다.

  • 심벌 참조에서 문자열로 기술된 완전한 이름에 해당하는 클래스를 찾을 수 있는지
  • 단순 이름과 필드 서술자와 일치하는 메서드나 필드가 해당 클래스에 존재하는지
  • 심벌 참조가 가르키는 클래스, 필드, 메서드의 접근 지정하가 현재 클래스의 접근을 허용하는지

운영 환경에서 실행할 때는 검증을 건너뛰기도 한다. 오래 기간 반복 사용해 왔다면 신뢰할 수 있다는 것이고 검증을 생략하면 가상 머신이 클래스 로딩하는 시간이 제법 단축된다.

준비

준비는 클래스 변수를 메모리에 할당하고 초깃값을 설정하는 단계다.

  • 인스턴스 변수가 아닌 클래스 변수만 할당된다. 인스턴스 변수는 객체가 인스턴스화될 때 객체와 함께 자바 힙에 할당된다.

  • 준비 단계에서 클래스 변수에 할당하는 초깃값은 해당 데이터 타입의 제로 값이다.

    Public static int value = 123;
    준비 단계를 마친 직후 value 값은 0이다.
    123을 할당하는 단계는 '클래스 초기화 단계' 이다.

    public static final int value = 123;
    final을 통해 ConstantValue 속성이 생성되면 준비단계에 바로 123을 할당한다.

해석

해석은 자바 가상 머신이 상수 풀의 심벌 참조를 직접 참조로 대체하는 과정이다.

심벌 참조는 가상 머신이 구현한 메모리 레이아웃과는 아무런 관련이 없다. 참조 대상이 반드시 가상 머신의 메모리에 로드되어 있을 필요도 없다. 메모리 레이아웃은 가상 머신 구현에 따라 달라질 수 있지만 심벌 참조는 달라지지 않는다.

직접 참조는 포인터, 상대적 위치(오프셋) 또는 대상의 위치를 간접적으로 가리키는 핸들이다. 가상 머신에 구현된 메모리 레이아웃과 밀접하게 관련된다. 똑같은 심벌 참조로부터 변환했더라도 직접 참조는 가상 머신에 따라 달라지는 게 보통이다.

초기화

초기화는 클래스 로딩의 마지막 단계다.
초기화 단계에 들어서면 JVM이 드디어 사용자 클래스에 작성된 자바 프로그램 코드를 실행하기 시작한다. 앞서 준비 단계에서는 모든 변수에 시스템이 정의한 초깃값인 0을 할당했다. 반면 초기화 단계에서는 클래스 변수와 기타 자원을 개발자가 프로그램 코드에 기술한 대로 초기화한다.

클래스 로더

클래스 로더는 당연하게도 클래스를 로딩하는 일을 하지만 그 일이 전부는 아니다.
클래스 로더를 빼놓고는 특정 클래스가 JVM에서 유일한지 판단할 수 없다. 같은 클래스를 다른 클래스 로더로 읽는다면 다른 클래스로 인식된다.

JVM 관점에서 클래스 로더의 종류는 2 가지뿐이다.

  • JVM 자체의 일부인 부트스트랩 클래스 로더 : hotspot JVM에서 C++로 구현
  • 그 외 모든 클래스 로더 : 추상 클래스인 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 패키지 같은 단순한 코드 컨테이너가 아니다. 자바 모듈 정의에는 코드 외에도 다음 내용이 포함된다.

  • requires : 다른 모듈에 대한 외존성 목록
  • exprots : 다른 모듈에서 사용할 수 있는 패키지 목록
  • open : 다른 모듈에서 리플렉션 API로 접근할 수 있는 패키지
  • uses : 현재 모듈이 사용할 서비스 목록
  • provides : 다른 모듈에 제공하는 서비스 목록

자바 모듈 시스템이 기존 클래스패스 방식과 호환되도록 하기 위해 JDK 9에서는 클래스패스에 해당하는 모듈패스 개념을 도입했다.
자바 설계진은 클래스패스 방식의 기존 자바 애플리케이션도 아무런 수정 없이 JDK 9 이상의 최신 환경에서 실행할 수 있도록 다음 규칙을 적용했다.

  • 클래스패스상의 JAR 파일용 접근 규칙
  • 모듈패스상의 모듈용 접근 규칙
  • 모듈패스상의 JAR 파일용 접근 규칙

JDK 9도 하위 호환을 위해 3계층 클래스 로더 아키텍처와 부모 위임 모델의 근간을 흔들지는 않았다. 주목할 만한 변화는 다음과 같다.

  • 확장 클래스 로더가 플랫폼 클래스 로더로 대체되었다.
  • 플랫폼 클래스 로더와 애플리케이션 클래스 로더가 더는 java.net.URLClassLoader로부터 파생되지 않는다.
    • 최신 로더는 모두 jdk.internal.loader.BuiltinClassLoader에서 파생된다.
profile
LEVEL UP

0개의 댓글