7장 - 클래스 로딩 메커니즘

7.1 들어가며

클래스 파일에 서술된 정보를 가상 머신이 이용하려면 먼저 로드해야 한다.

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

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

자바가 동적 확장 언어 기능을 제공할 수 있는 이유는 런타임에 이루어지는 동적 로딩과 동적 링킹 덕분이다.

  • 예를 들어, 애플리케이션 코드를 인터페이스 중심으로 작성해 두면 실제 구현 클래스를 결정하는 일은 실행 시까지 미룰 수 있다.

  • 또한 클래스로더를 활용하면 실행 중인 프로그램의 일부를 네트워크를 통해서 바이너리스트림으로 읽어 올 수 있다.

7.2 클래스 로딩 시점

가상 머신의 메모리에 로드되는 걸 시작으로 다시 언로드 될 때까지 아래의 과정을 거친다.

  1. 로딩 (Loading)

  2. 링킹 (Linking) -> 하위 과정은 반드시 순서를 지켜야 한다.

    1. 검증 (Verification)

    2. 준비 (Preperation)

    3. 해석 (Resolution)

  3. 초기화 (Initialization)

  4. 사용 (Using)

  5. 언로딩 (Unloading)

클래스는 애플리케이션 실행 중 해당 클래스가 명시적으로 필요해지는 시점에 로드되고 초기화된다. 이는 JVM이 필요할 때만 리소스를 로드하여 효율성을 높이는 "지연 로딩(Lazy Loading)" 방식이다.

7.3 클래스 로딩 처리 과정

7.3.1 로딩 (Loading)

자바 가상 머신은 로딩 단계에서 다음 3가지 작업을 수행해야 한다.

  1. 완전한 이름을 보고 해당 클래스를 정의하는 바이너리 스트림(바이트코드)을 가져온다.

  2. 바이트 스트림으로 표현된 정적인 저장 구조를 메서드 영역에서 사용하는 런타임 데이터 구조로 변환한다.

  3. 로딩 대상 클래스를 정의하는 java.lang.Class 객체를 힙 메모리에 생성한다.

    1. 이 Class 객체는 애플리케이션이 메서드 영역에 저장된 타입 데이터를 활용할 수 있는 통로가 된다.

    2. 애플리케이션 코드에서는 리플렉션과 같은 방식을 통해서 Class 객체를 생성한다.

"자바 가산 머신 명세" 가 이 요구사항을 너무 세세하게 정의하지 않은 까닭에 가상 머신 구현자와 자바 애플리케이션이 취할 수 있는 액션의 범위가 넓다.

7.3.2 검증 (Verification)

검증은 링킹 과정 중 첫번째 단계로 검증의 목적은 다음 2가지 이다.

  1. 클래스 파일의 바이트 스트림에 담긴 정보가 "자바 가상 머신 명세" 에서 규정한 모든 제약을 만족하는지 확인한다.

  2. 이 정보를 코드로 변환해 실행했을 때 자바 가상 머신 자체의 보안을 위협하지 않는지 확인한다.

JVM 은 바이트코드를 실행하는데 중간에 바이트코드를 물리적으로 수정할 수 있다. 때문에, 바이트코드를 검증하지 않는다면 오류가 있거나 악의적으로 작성된 바이트코드가 실행되어 프로그램 전체를 해칠 수 있다.

결국 바이트코드 검증은 결국 자바 가상 머신이 스스로를 보호하기 위한 필수 조치인 셈이다.

검증 단계는 자바 가상 머신이 악성 코드로부터 자신을 보호하기 위해서 엄격하게 진행해야 하며 코드의 양적 측면과 실행 성능 측면에서 클래스 로딩 과정 중 매우 큰 비중을 차지한다.

검증 단계는 매우 중요하지만 필수는 아니다. 그래서 프로그램에서 실행하는 모든 코드를 신뢰할 수 있다면 프로덕션 환경에서 실행할 때는 건너뛰기도 한다.

7.3.3 준비 (Preperation)

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

준비 단계는 혼란스러운 개념이 두 가지 등장하니 먼저 정리하고 시작하자.

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

  2. 준비 단계에서 클래스 변수에 할당하는 초기값은 해당 데이터 타입의 기본값이다.

    1. ex, public static int value = 123 에서 초기값은 0

    2. 값을 할당하는 일은 '클래스 초기화 단계' 에서 이루어진다.

    3. 하지만, final 이 붙어있다면 준비 단계에서 값을 할당한다.

7.3.4 해석 (Resolution)

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

  • 심벌 참조 : 논리적인 참조

  • 직접 참조 : 물리적인 참조

동일한 심벌 참조에 대해서도 해석 요청이 여러 번 이루어지는게 보통이므로 가상 머신은 첫 번째 해석 결과를 캐시해 사용한다.

7.3.5 초기화 (Initialization)

초기화는 클래스 로딩의 마지막 단계이다.

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

  • 초기화 대상은 정적 변수와, 정적 블럭이다.

초기화는 클래스 로딩의 모든 단계 중에서 보통의 프로그램 개발자가 실제로 하는 작업에 가장 가깝다.

7.4 클래스 로더

7.4.1 클래스와 클래스 로더

클래스 로더는 당연하게도 클래스를 로딩하는 일을 하지만 그 일이 전부는 아니다.

각 클래스 로더는 독립적인 클래스 이름 공간을 지니기 때문에 클래스 로더를 빼놓고는 특정 클래스가 자바 가상 머신에서 유일한지 판단할 수 없다. 달리 표현하면 어떤 두 클래스가 '동치인가' 여부는 두 클래스 모두 같은 클래스 로더로 로드했을 때만 의미가 있다.

서로 다른 클래스 로더로 읽어 들였다면, 비록 같은 가상 머신이고 똑같은 클래스 파일로부터 로드했더라도 다른 클래스로 인식된다.

7.4.2 부모 위임 모델

자바 가상 머신 관점에서 클래스 로더의 종류는 다음과 같이 딱 두 가지뿐이다.

  • 자바 가상 머신 자체의 일부인 부트스트랩 클래스 로더 : 핫스팟 가상 머신에서는 C++ 로 구현했다.

  • 그 외 모든 클래스 로더 : 추상 클래스인 java.lang.ClassLoader 를 상속하여 자바로 구현하며, 가상 머신 외부에 독립적으로 존재한다.

한편 자바 개발자 관점에서는 클래스 로더를 더 잘게 나눌 수 있다. 자바는 JDK 1.2 부터 3계층 클래스 로더인 부모 위임 클래스 로딩 아키텍처를 유지해 왔다. 비록 조금씩 수정되다가 모듈 개념을 도입하면서 변경되었지만 뼈대는 그대로다.

이번 절에서는 JDK 8 까지 유지된 3계층 클래스 로더와 부모 위임 모델(parents delegation model) 이 무엇인지 알아보겠다. 이 시기 자바 애플리케이션은 대부분 시스템이 제공하는 다음 세 가지 클래스 로더를 통해 로드되었다.

  • 부트스랩 클래스 로더 : 자바 가상 머신이 클래스 라이브러리로 인식하는 파일들을 로드하는 일을 책임진다. 부트스트랩 클래스 로더는 자바 프로그램에서 직접 참조를 할 수 없다.

  • 확장 클래스 로더 : 자바로 구현되었다. '확장' 클래스 로더라는 이름에서 유추할 수 있듯이 자바 시스템의 클래스 라이브러리를 확장하는 메커니즘이다. JDK 개발 팀은 가상 머신 사용자가 ext 디렉터리에 범용 클래스 라이브러리를 두어 자바 SE 의 기능을 확장할 수 있도록 했다. (JDK 9 부터는 모듈 시스템을 통한 확장 메커니즘으로 대체되었다). 확장 클래스 로더는 자바 코드로 구현되었기 때문에 개발자가 프로그램 안에서 직접 사용할 수 있다.

  • 애플리케이션 클래스 로더 : 클래스패스상 클래스 라이브러리들을 로드하는 역할을 하며, 개발자가 자바 코드에서 직접 사용할 수 있다. 애플리케이션에서 클래스 로더를 따로 만들어 이용하지 않는 경우 이 로더가 기본 클래스 로더가 된다.

JDK 8 까지의 자바 애플리케이션들은 이 세 클래스 로더가 적절히 협력하여 로딩을 책임진다. 그리고 필요시 사용자가 직접 만든 클래스 로더를 추가할 수 있다.

위 그림과 같은 클래스 로더 간 계층 관계를 클래스 로더들의 부모 위임 모델이라고 한다. 가장 위에 자리한 부트스트랩 클래스 로더 외에는 부모가 있어야 한다. 이 때 부모-자식 관계는 상속보다는 '주로' 콤포지션(상속) 관계로 구현하여 부모 로더의 코드를 재사용한다.

부모 위임 모델이 어떻게 동작하는지 살펴보자.

  1. 클래스 로딩을 요청받은 클래스 로더는 처음부터 클래스 자체를 로드하려 시도하지 않는다. 그 대신 수준에 맞는 상위 클래스 로더로 요청을 위임한다. 따라서 모든 로드 요청은 우선 최상위인 부트스트랩 클래스 로더로 넘겨진다.

  2. 상위 로더가 자신이 처리할 요청이 아니라고 판단하면, 즉 요청받은 클래스가 자신의 검색 범위에 없다면 비로서 하위 로더가 시도한다.

    1. 클래스 로더를 부모 위임 모델로 구성하면 자바 클래스들이 자연스럽게 클래스 로더의 계층 구조를 따르게 된다는 이점이 있다.

    2. 예를 들어, rt.jar 에 포함된 java.lang.Object 클래스의 로딩은 어떤 클래스 로더에 요청하더라도 최상위인 부트스트랩 클래스 로더가 처리한다. 즉, 프로그램이 아무리 많은 클래스 로더를 사용하더라도 Object 클래스는 모두 동일한 클래스임이 보장된다.

부모 위임 모델을 따르지 않으면 각 클래스 로더가 자체적으로 로드를 수행한다. 사용자가 직접 "java.lang.Object" 라는 이름의 클래스를 작성하여 클래스패스에 넣어 버리면 서로 다른 Object 클래스들이 생겨나 버린다. 그러면 자바 타입 시스템에서 가장 기본이 되는 동작들을 보장받지 못해 애플리케이션이 엉망이 될 것이다. (이런 시스템 불안정을 초래하는 보안적 이슈가 크기 때문에 부모 위임 모델을 사용한다)

7.5 자바 모듈 시스템

JDK 9 에 도입된 모듈 시스템은 자바 기술에 있어 중요한 개선이다.

클래스패스에 기초하던 JDK 8 까지의 의존성 관리 방식은 안정성에 문제가 있었다. 자바 모듈 시스템은 이 문제를 개선한다. 이전까지는 필요한 타입이 클래스패스에 없더라도 프로그램이 그 타입을 처음 사용하려 할 때에서야 비로서 예외가 보고되었다.

반면 JDK 9 부터는 모듈이 의존하는 다른 모듈들을 명시할 수 있어서, 필요한 의존성이 모두 갖춰졌는지 애플리케이션 개발 단계에서 확인할 수 있다. 의존성이 누락되었다면 애플리케이션은 시작 자체를 못하기 때문에 런타임 예외를 상당 부분 피할 수 있다.

7.5.1 모듈 호환성

자바 모듈 시스템이 기존 클래스패스 방식과 호환되도록 하기 위해 JDK 9 에서는 클래스패스에 해당하는 모듈패스 개념을 도입했다. 간단히 말하면, 클래스 라이브러리의 위치에 따라 모듈인지 아니면 전통적인 JAR 패키지인지 결정된다.

자바 설계진은 몇가지 규칙을 통해 옛 클래스패스 방식을 따르는 자바 애플리케이션도 JDK 9 로 매끄럽게 업그레이드 할 수 있다. (설명은 패스..)

7.5.2 모듈화 시대의 클래스 로더

JDK 9도 하위 호환을 위해 JDK 1.2 이후 20년 이상 유지해 온 3계층 클래스 로더 아키텍처와 부모 위임 모델의 근간을 흔들지는 않았다. 다만 모듈 시스템을 원활하게 구현하기 위해서 몇가지 주목할만한 변화가 있었다.

  1. 확장 클래스 로더가 플랫폼 클래스 로더로 대체되었다.

  2. 플랫폼 클래스 로더와 애플리케이션 클래스로더가 더는 java.net.URLClassLoader 로부터 파생되지 않는다.

  3. JDK 9 는 3계층 클래스 로더와 부모 위임 모델을 여전히 유지하지만 클래스 로딩의 위임 관계에는 변화를 주었다.

    1. 클래스 로딩을 요청받은 플랫폼 및 애플리케이션 클래스 로더는 부모 로더에 위임하기 전에 해당 클래스가 특정 시스템 모듈에 속하는지 확인하고 위임한다.

Last updated