ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JVM 클래스로더 알아보기
    Java 2025. 1. 11. 02:34

    JVM 클래스로더를 알아보자.

    Goal

    • 클래스로더의 역할 및 개요에 대한 학습
    • 클래스로더가 클래스를 로딩하는 과정 알아보기

    클래스로더

    JVM이 클래스 로딩을 하는 과정은 크게 로딩 - 링크 - 초기화 단계로 이루어진다.
    클래스 로더는 로딩 과정에서 클래스파일을 JVM 내부로 읽어들이는 역할을 한다.

     

    클래스 로더의 종류는 크게 다음과 같다.

    1. 부트스트랩 클래스로더(Bootstrap ClassLoader)
    2. 확장 클래스로더(Extension ClassLoader)
    3. 애플리케이션 클래스로더 또는 시스템 클래스 로더(System ClassLoader)

    클래스 로더의 특징은 다음과 같다.

    • 유일성
      • 같은 클래스라도 클래스로더 마다 로드한 결과는 다르다.
      • 하나의 클래스를 두개의 서로 다른 클래스로더로 로딩을 하면 결과 값은 동일한 결과가 아니게 된다.
      • 클래스는 네임스페이스와 클래스를 페어로 식별하기 때문이다.
    • 부모 위임 모델
      • 로드된 클래스가 있다면 반환, 없다면 상위 클래스로 요청을 위임한다.
      • 상위 클래스도 똑같이 동작하다가, 최상위 클래스로더라면 하위 클래스에게 요청을 위임한다.

    클래스로더는 클래스 로딩을 하기 전 로딩된 클래스가 있는 지 확인 후 없다면 상위 클래스에게 로딩을 위임하는 부모 위임 모델의 특징을 가지고 있다.

     

    • 로딩된 클래스가 있는 지 확인을 한다.
      • 클래스를 로딩하는 작업은 로딩-검증-초기화를 거치는 비싼 작업이다. 여러번 로딩할 필요가 없다.
    • 로딩된 클래스가 없다면 상위 클래스로더에게 위임한다.
      • 상위 클래스로더가 없다면 본인이 로딩할 수 있는 클래스인 지 확인 후 로딩 혹은 하위 클래스 로더에게 위임한다.
      • 이때 주의해야할 점은 최초에 요청이 들어온 곳보다 아래로 요청이 위임될 수 없다.

    용어에 대한 생각

    상위 클래스로더는 하위 클래스로더의 클래스에 접근하지 못하는 것을 Visibility Principle, 가시성 원칙, 제약 조건이라고 설명하는 글을 정말 많이 봤다. 그래서 자바 가상 머신 명세에 나와있는 어떠한 원칙인 줄 알고 참고하기 위해 공식 문서를 포함하여 관련 논문까지 찾아봐도 관련된 용어를 찾을 수 없었다. 그리고 문득 궁금해졌다. 특징도 아니고, 왜 원칙, principle 이란 말이 붙게 되었는 지.

     

    부모 위임 모델의 목적은 하나의 클래스 파일이 메모리 상에서 하나만 존재하기 위함이다.
    예를 들어 java.lang.Object는 모든 클래스의 부모 클래스로서 가장 많이 쓰이는 클래스이다. 이런 클래스가 메모리 상에서 두개 이상 존재한다면 문제가 된다. 어떤 클래스를 참조해서 사용해야되는 지, 메모리 누수 등등. 또한 두개 이상 존재할 필요가 있을까??

     

    그리고 클래스로더가 계층 구조를 갖는 이유도 분명하다. 계층마다 처리해야할 클래스가 명확히 나뉘어 있기 때문이다. 부트스트랩 클래스로더는 자바 실행 환경에 필요한 최소한의 클래스를 로드하는 책임을 갖는다. 객체 지향 세계에서는 역할과 책임을 나누는 것이 중요하다. 클래스로더에 계층 구조가 적용된 이유도 각 계층의 역할과 책임이 분명하기 때문이라고 생각한다. 하위 클래스가 요청을 우선적으로 상위 클래스로 위임하는 이유도 클래스의 유일성을 보장하기 위함이다.

     

    부트스트랩 클래스로더는 자바 환경에 필요한 최소한의 클래스들을 로드하는 책임을 가지고 있다.
    그런데 이 부트스트랩 클래스로더가 외부 라이브러리의 클래스를 로드하는 책임을 갖게 된다면, 책임이 불분명한 거대한 클래스로더가 되어버릴 것이다. 레이어 별로 클래스들의 계층을 나누고 나눠진 계층을 처리하는 클래스로더를 구분했다고 생각이 든다. 그러면 자연스럽게 상위 계층은 하위 계층의 클래스를 참조할 이유가 없다. 이러한 당연한 결과를 가시성 원칙이라는 말로 어떻게 보면 자바 가상 머신 명세에 정의된 원칙처럼 와전된 게 아닐까 하고 조심스럽게 추측 해본다.


    소스코드 분석

    java.lang.ClassLoader 의 소스코드를 확인해봤다.

     

    java.lang.ClassLoader 의 주요 메서드인 loadClass 를 살펴보자. 해당 함수는 재귀함수의 형태로 함수 내에서 본인과 같은 타입의 동일한 메서드를 재귀적으로 호출하고 있다. 다음은 단계 별로 주요 동작을 요약했다.

      1. 로드 된 적이 있는 클래스인 지를 먼저 확인한다. 여기서 만약 로드 된 적이 있는 클래스라면 아래의 조건문은 false 가 되고 로드 되었던 결과가 리턴된다. 이때 확인하는 범위는 해당 클래스로더의 네임스페이스 범위까지이다. 하위 클래스로더가 상위 클래스로더에 접근이 가능하다고 해서 여기서 접근이 가능한 것이 아니다. 하위 클래스로더에서 상위 클래스로더의 클래스에 접근은 재귀적으로 일어나는 것이다.
      2. 로드 된 적이 없는 클래스라면 부모 클래스 로더가 존재하는 지 확인 후, 존재한다면 부모 클래스 로더의 loadClass 를 호출한다. 클래스로더는 재귀적인 형태로 되어있으며, 필드에 자기 자신과 같은 타입을 참조하고 있다. 이 필드가 null 이라면 해당 클래스로더는 최상위 클래스로더인 부트스트랩 클래스로더다. 따라서 findBootstrapClass 을 호출하여 부트스트랩 클래스로더를 호출한다. (부트스트랩 클래스로더는 C++ 로 구현되어 있다. 따라서 자바 클래스로 직접적인 호출이 불가하다. 위의 코드를 따라가보면 native 코드를 호출하는 것을 확인할 수 있다.)
      3. 여전히 c의 결과가 null 이라면 findClass 을 호출하여 클래스로딩을 시도한다. 이 과정에서 클래스를 로딩하지 못하면 ClassNotFoundException 에러를 던진다. 바깥으로 에러를 던지면, 자신을 호출한 함수의 try 블럭으로 가서 다시 코드를 실행한다. 즉, 자식에게 요청을 위임하는 구조가 되는 것이다.

    그림을 통해 살펴보자.

    • JVM에서 스레드는 스택이란 공간을 고유하게 관리한다.
    • 그리고 스택은 메서드를 호출할 때마다 스택 프레임을 생성한다.
    • 스택 프레임은 Local Variables(지역 변수 배열), Operand Stack(피연산자 스택), Run-time Constant Pool 참조로 구성되어 있다.

    #1

    • try 블럭 안에서 재귀 함수를 호출하면 아래처럼 스택 프레임이 생성된다.

    #2

    • 에러를 처리할 수 없다면 바깥으로 에러를 던지면서 스택 프레임은 POP 된다.
    • 그럼 하단에 있는 스택 프레임에게 에러는 전달된다.
    • 이 과정이 부모가 자식에게 요청을 위임하는 코드가 되는 것이다.

    만약 마지막 클래스로더도 클래스 로딩에 실패하게 된다면 에러를 처리할 클래스가 없으니 ClassNotFoundException 예외를 날리고 애플리케이션이 종료된다.

    JVM이 예외를 핸들링 하는 방법

    정답은 바이트코드의 Code_attribute 에 있다.

    Code_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 max_stack;
        u2 max_locals;
        u4 code_length;
        u1 code[code_length];
        u2 exception_table_length;
        {   u2 start_pc; // 시작 PC 주소
            u2 end_pc; // 종료 PC 주소
            u2 handler_pc; // 핸들러 PC 주소
            u2 catch_type; // 처리할 에러 타입
        } exception_table[exception_table_length];
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }
    • 스택 프레임은 런타임 상수풀을 참조하여 코드를 실행한다.
    • 그리고 이 코드는 Code_attribute 구조를 가진다.
    • try-catch 문처럼 예외처리를 했다면 exception table에 대한 정보도 존재한다.
    • catch_type 에 맞는 예외가 발생하면 exception table 을 참조하여 handler_pc로 이동하게 된다.
    • 만약 예외를 처리할 수 없다면 예외 객체를 호출자에게 전달한다.

    공식 문서의 설명

    'Java' 카테고리의 다른 글

    JVM 클래스 로딩 과정 알아보기  (0) 2025.01.09
    JVM 바이트코드 알아보기  (1) 2025.01.04
    자바와 코틀린  (0) 2024.08.29
    커스텀 어노테이션  (0) 2024.02.13
    OpenAPI 데이터베이스 저장  (0) 2023.05.02
Designed by Tistory.