ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JVM] 가상 머신 실행 서브시스템 (2) - 클래스 로딩 메커니즘
    DevBook/JVM 밑바닥까지 파헤치기 2025. 4. 1. 00:35

    들어가기 전,

    • 클래스 파일에 서술된 정보를 가상 머신이 사용하려면 먼저 로드해야
    • 가상 머신이 클래스 파일을 로드하는 방법, 그 정보를 가상 머신 안에서 활용하는 방법에 대해 설명함
    • JVM은 클래스를 설명하는 데이터를 클래스 파일로부터 메모리로 읽어 들이고 그 데이터를 검증, 변환, 초기화하고 나서 최종적으로 가상 머신이 곧바로 사용할 수 있는 자바 타입을 생성
      • 이 과정을 가상 머신의 클래스 로딩 메커니즘이라고 함
    • 자바에서는 클래스 로딩, 링킹, 초기화가 모두 '프로그램 실행 중에' 이루어짐
    • 자바가 동적 확장 언어 기능을 제공할 수 있는 것은 런타임에 이루어지는 동적 로딩과 동적 링킹 덕분임
    • 이와 같이 어플리케이션을 동적으로 조합하는 기법은 가장 기초적인 예인 JSP부터 비교적 복잡한 OSGi 기술에 이르기까지 자바 프로그램에서 널리 사용되어 왔음. 이는 전부 자바 언어의 런타임 클래스 로딩 기술 덕분임

     

    두 가지 규칙

    • 실질적으로 클래스 파일 하나는 자바 언어에서 말하는 클래스 하나 또는 인터페이스 하나를 나타낼 수 있다.
      • 여기서 '타입'이라는 표현을 쓸 때는 클래스와 인터페이스 하나를 나타낼 수 있음
    • '클래스 파일'이라고 하면 디스크에 존재하는 특정 파일이 아니라 일련의 바이너리 바이트 스트림을 뜻한다.
      • 즉, 클래스 파일은 SSD, 네트워크, 데이터베이스, 메모리 등 어디에도 존재할 수 있음

     

    클래스 로딩 시점

    타입의 생애 주기는 아래와 같음

    • 가상 머신의 메모리에 로드되는 걸 시작으로 다시 언로드될 때까지 위와 같은 과정을 거침
    • 이 중 검증, 준비, 해석 단계를 묶어 링킹이라고 함
    • 로딩, 검증, 준비, 초기화, 언로딩은 반드시 순서대로 진행해야 함
    • 해석 단계는 때에 따라서는 초기화 후에 시작할 수 있음
      • 자바의 런타임 바인딩(동적 바인딩)을 지원하기 위함
    • 단계별 순서의 기준은 '시작' 시점임
      • 때로는 한 단계를 진행하는 중간에 다음 단계를 호출해 시작시키는 등 여러 단계가 병렬로 진행되기도 함

     

    추가) 초기화 단계가 즉시 시작되어야 하는 상황

    더보기
    • <자바 가상 머신 명세>는 클래스 로딩 과정의 첫 단계인 '로딩'을 정확히 어떤 상황에서 시작해야 하는지 명시하지 않았음
    • 반면 초기화 단계는 즉시 시작되어야 하는 여섯 가지를 엄격히 규정
    • 시나리오들이 설명하는 동작을 타입에 대한 능동 참조(active reference)라고 함
    • 반대로 타입 초기화를 촉발하지 않는 그 외의 모든 참조 방식은 수동 참조(passive refernce)라고 함

     

    수동 참조 예시

    1)

    • 정적 필드를 참조할 때는 필드를 직접 정의한 클래스만 초기화됨
    • 상위 클래스에 정의된 필드를 하위 클래스를 통해 참조하면 하위 클래스는 초기화되지 않음
      • 이때 하위 클래스의 로딩과 검증 단계 촉발 여부는 가상 머신 구현하기 나름

    2)

    • 위 코드를 실행해도 상위 클래스 초기화가 수행되지 않음
      • org.fenixsoft.jvm.chapter7.SuperClass 클래스의 초기화 단계가 촉발되지 않음
    • 하지만 이 코드는 [Lorg.fenixsoft.jvm.chapter7.SuperClass라는 다른 클래스의 초기화 단계를 촉발함
      • 사용자 코드에서는 유효하지 않은 이름
      • JVM이 java.lang.Object로부터 곧바로 상속하여 자동으로 생성한 하위 클래스
      • 이 클래스 생성을 촉발하는 바이트코드 명령어는 anewarray
    • 이 클래스의 정체는 원소 타입(element type)이 org.fenixsoft.jvm.chapter7.SuperClass인 '일차원 배열'임
      • 자바 소스 코드에서 배열이 제공하는 속성과 메서드를 구현한 실체가 바로 이 클래스
      • public으로 설정된 length 속성과 clone() 메서드만 사용자 코드에서 직접 이용할 수 있음
      • 자바 언어의 배열이 C/C++ 배열보다 안전한 주된 이유는 배열 원소로 직접 접근하지 못하도록 이 클래스가 감싸주기 때문임
      • 자바 언어는 배열 범위를 벗어나 접근하련느 코드를 발견하면 ArrayIndexOutOfBoundsException을 던져 잘못된 메모리 접근을 막음

    3)

    • 코드를 실행해도 "ConstClass 초기화!"는 출력되지 않음
    • 자바 소스 코드에서는 ConstClass 클래스에 정의된 상수인 HELLO_WORLD를 이용하고 있지만, 컴파일 과정에서 상수 전파 최적화가 이루어지기 때문임
    • 그 결과 상수의 값 "hello world"는 NotInitialization_3 클래스의 상수 풀에 직접 저장되고, ConstClass.HELLO_WORLD를 참조하는 코드는 NotInitialization_3 클래스 자체의 상수 풀을 참조하도록 변경됨
      • 즉, NotInitialization_3 클래스 파일에는 ConstClass 클래스의 내용을 가리키는 심벌 참조가 만들어지지 않음

     

    추가) 인터페이스 로딩

    더보기
    • 인터페이스 로딩 과정은 클래스 로딩과는 살짝 달라서 몇 가지 전용 명령어가 준비되어 있음
    • 인터페이스 초기화 역시 클래스 초기화와 공통된 부분이 있음
    • 컴파일러는 인터페이스가 정의한 멤버 변수들을 초기화하는 용도로 여전히 클래스 생성자인 <clinit>() 메서드를 생성함
    • 인터페이스 초기화에는 상위 인터페이스 초기화가 필요 없음
      • 클래스를 초기화하려면 먼저 상위 클래스를 모두 초기화해야 함
      • 상위 인터페이스 초기화는 해당 상위 인터페이스가 실제로 사용될 때 이루어짐

     

    클래스 로딩 처리 과정

    1) 로딩

    • '클래스 로딩'의 전체 과정 중 한 단계
    • JVM은 로딩 단계에서 다음 세 가지 작업을 수행해야 함
      1. 완전한 이름을 보고 해당 클래스를 정의하는 바이너리 바이트 스트림(클래스 파일)을 가져온다.
      2. 바이트 스트림으로 표현된 정적인 저장 구조를 메서드 영역(Method Area)에서 사용하는 런타임 데이터 구조로 변환한다.
      3. 로딩 대상 클래스를 표현하는 java.lang.Class 객체를 힙 메모리에 생성한다.
        • 이 Class 객체는 어플리케이션이 메서드 영역에 저장된 타입 데이터를 활용할 수 있게 하는 통로가 됨
    • <자바 가상 머신 명세>에서 로딩 단계의 구현 방식을 구체적으로 명시하지 않았기 때문에 JVM은 로딩 단계에서 광범위한 작업을 수행할 수 있게 되었음
      • 예시) 동적 프록시 기술
        • java.lang.reflect.Proxy의 ProxyGenerator.generateProxyClass() 메서드는 지정한 인터페이스에 대해 "*$Proxy" 형태의 프록시 클래스에 해당하는 바이너리 바이트 스트림을 생성해준다.
    • 참고)
      • 로딩 단계와 링킹 단계의 일부 동작은 서로 중첩되어 진행될 수 있음 (로깅 단계에서 시작한 작업들이 완료되기 전에 링킹 단계 작업이 시작될 수 있음)
      • 하지만 시작 시간 기준으로는 항상 로딩 단계가 먼저 일어남

     

    로딩 방식

    • 배열 외 타입(클래스, 인터페이스)의 로딩
      • 로딩 단계는 JVM에 내장된 부트스트랩 클래스 로더를 사용하거나 사용자 정의 클래스 로더를 사용해 수행할 수 있음
      • ClassLoader의 findClass() 또는 loadClass() 메서드를 오버라이딩하면 바이트 스트림을 얻는 방법을 통제할 수 있음
    • 배열 로딩
      • 배열 클래스는 클래스 로더가 생성하지 않고 JVM이 직접 메모리에 동적으로 생성
        • 단, 배열 클래스의 원소 타입은 클래스 로더를 통해 로드
          • ex) int[][] 타입 배열의 원소 타입은 int
      • 배열 클래스(이하 C로 칭함) 생성 규칙
        • 배열의 컴포넌트 타입이 참조 타입이면, 로딩 과정을 재귀적으로 수행하여 컴포넌트 타입을 로딩한다.
          • 배열 C는 컴포넌트 타입을 로드하는 클래스 로더의 이름 공간(namespace)에 자리하게 됨
            • 타입은 클래스 로더에서 유일해야 함 
          • *컴포넌트 타입: int[][] 타입 배열의 컴포넌트 타입은 int[]이며 원소 타입은 여전히 int
            • int[] 타입 배열에서는 원소 타입과 컴포넌트 타입이 똑같이 int
        • 배열의 컴포넌트 타입이 참조 타입이 아니면, JVM은 배열 C를 부트스트랩 클래스 로더에 맡긴다.
        • 배열 클래스의 접근성은 해당 컴포넌트 타입과 같다.
          • 컴포넌트 타입이 참조 타입이 아닌 배열 클래스라면, 기본적으로 public이라서 모든 클래스와 인터페이스에서 접근할 수 있음

     

    정리하면,

    • 로딩 단계가 끝나면 바이너리 바이트 스트림(클래스 파일)은 JVM이 정의한 형식에 맞게 메서드 영역에 저장됨
      • Method Area에 저장되는 데이터 구조는 아래의 정보를 포함함
        • 클래스 이름, 부모 클래스
        • 필드 정의, 메서드 정의, 접근 제어자, 인터페이스 목록
        • 런타임 상수 풀 등
          • *클래스 파일의 상수 풀 --> JVM 내부 구조로 파싱 --> Method Area의 런타임 상수 풀로 저장
          • *런타임 상수 풀은 클래스마다 하나씩 존재함
      • Method Area는 클래스 메타데이터 저장소
    • 타입 정보(클래스, 인터페이스 정보)를 메서드 영역에 올바르게 저장한 다음에는 해당 java.lang.Class 객체를 자바 힙에 초기화 함
      • JVM은 로딩한 클래스마다 java.lang.Class 타입의 객체를 하나 생성함. 이 객체는 자바 힙에 올라가고, 해당 클래스에 대한 메타 정보를 Method Area로부터 참조함
      • Class 객체는 프로그램에서 메서드 영역 안의 타입 데이터에 접근하기 위한 통로 역할을 함

     

    2) 검증

    • 링킹 과정 중 첫 번째 단계
    • 두 가지 목적
      1. 클래스 파일의 바이트 스트림에 담긴 정보가 <자바 가상 머신 명세>에서 규정한 모든 제약을 만족하는지 확인한다.
      2. 이 정보를 코드로 변환해 실행했을 때 JVM 자체의 보안을 위협하지 않는지 확인한다.
    • 검증은 크게 4단계를 수행함 (자바 SE 7용 <자바 가상 머신 명세>에서부터 제약 조건과 검증 규칙이 구체화 됨)
      1. 파일 형식 검증
      2. 메타데이터 검증
      3. 바이트코드 검증
      4. 심벌 참조 검증
    • 검증 단계는 매우 중요하지만 필수는 아님
      • 프로덕션 환경에서 실행할 때는 프로그램에서 실행하는 모든 코드를 신뢰할 수 있다면 검증을 건너뛰기도 함
        • -Xverify:none 매개변수 지정

     

    추가) 검증 주요 4단계

    더보기
    1. 파일 형식 검증
      • 바이트 스트림이 클래스 파일 형식에 부합하고 현재 버전의 가상 머신에서 처리될 수 있는지 확인
      • 검증 단계의 주된 목적은 입력 바이트 스트림이 올바르게 해석되어 메서드 영역에 저장되어 있는지, 파일 형태가 자바 타입 정보 설명에 대한 요구사항을 준수하는지 확인하는 것
      • 이 단계의 검증은 바이너리 바이트 스트림을 대상으로 이루어지며, 검증을 통과하면 바이트 스트림이 JVM 메모리 중 Method Area에 저장됨
        • 이어지는 3개 단계는 모두 메서드 영역에 저장된 구조가 대상이 됨 (즉, 이후 단계에서는 바이트 스트림을 직접 읽지 않는다는 뜻)
    2. 메타데이터 검증
      • 바이트 코드로 설명된 정보의 의미를 분석하여 서술된 정보가 <자바 가상 머신 명세>의 요구사항을 충족하는지 확인
      • 주된 목적은 클래스의 메타데이터 정보에 대한 의미론적 검증을 수행하여 <자바 언어 명세>와 일치하지 않는 메타데이터가 섞여 있지 않은지 확인하는 것
    3. 바이트코드 검증
      • 전체 검증 과정에서 가장 복잡한 단계
      • 주된 목적은 데이터 흐름과 제어 흐름을 분석하여 프로그램의 의미가 적법하고 논리적인지 확인하는 것
      • 앞서 메타데이터 검증에서 데이터 타입 관련 검증을 마친 후, 이번 단계에서는 클래스의 메서드 본문(클래스 파일의 Code 속성)을 분석함
        • 메서드가 런타임에 가상 머신의 보안을 위협하는 동작을 하지 않는지 확인하는 것
      • 데이터 흐름 분석과 제어 흐름 분석은 굉장히 복잡하기 때문에 바이트코드 검증 단계가 자칫 길어질 수 있어, JDK 6 이후로는 가능한한 많은 유효성 검사를 javac 컴파일러로 옮김
    4. 심벌 참조 검증
      • 가상 머신이 심벌 참조를 직접 참조로 변환할 때 수행됨
        • 이 변환은 링킹의 세 번째 단계인 해석 단계에서 일어남
      • 현재 클래스가 참조하는 특정 외부 클래스, 메서드, 필드, 그 외 자원들에 접근할 권한이 있는지 확인함
        • JDK 9부터 자바에 모듈 개념이 도입되면서 public 타입이어도 프로그램 어디에서든 접근할 수는 없게 됨. 따라서 모듈 간 접근 권한까지 확인해야 함
      • 주된 목적은 해석을 제대로 수행할 수 있는지 확인하는 것
        • 해당 검증을 통과하지 못하면 JVM이 IncompatibleClassChangeError의 하위 예외를 던짐

     

    3) 준비

    • 클래스 변수(정적 변수)를 메모리에 할당하고 초기값을 설정하는 단계
      • 개념적으로는 클래스 변수들이 사용하는 메모리를 메서드 영역에 할당해야 하지만, 메서드 영역 자체는 JVM이 정의한 논리적 영역임
      • JDK 8부터 클래스 변수는 클래스 객체와 함께 자바 힙에 저장됨 - 참고
        • 논리적으로는 메서드 영역에 저장되지만, 실제 저장은 자바 힙에 저장되는 것
      • "클래스 변수는 메서드 영역에 존재한다"라는 말은 논리적으로 그렇다는 뜻
    • 주의
      • 인스턴스 변수(멤버 변수)가 아닌 클래스 변수만 할당된다.
        • 인스턴스 변수는 객체가 인스턴스화될 때 객체와 함께 자바 힙에 할당됨
      • 준비 단계에서 클래스 변수에 할당하는 초기값은 해당 데이터 타입의 제로값이다.
        • ex) public static int value = 123;
          • 준비 단계를 마친 직후 value 변수에 할당된 초기값은 123이 아닌 0
          • 123을 할당하는 putstatic 명령어는 클래스 생성자인 <clinit>() 메서드에 포함되어 있고, 준비 단계에서는 어떠한 자바 메서드로 아직 실행되지 않은 상태이기 때문
          • 123을 할당하는 일은 '클래스 초기화 단계'에 가서야 이루어짐
        • 예외)
          • 클래스 필드의 필드 속성 테이블에 ConstantValue 속성이 존재한다면, 준비 단계에서 변수의 초기값으로 ConstantValue 속성이 지정한 값을 할당함
          • ex) public static final int value = 123;
            • 해당 코드를 컴파일하면 javac가 value 변수를 위한 ConstantValue 속성을 생성함
            • 가상 머신은 준비 단계에서 ConstantValue에 설정된 값을 value에 할당함

     

    4) 해석

    • JVM이 (클래스 메타데이터 내) 상수 풀의 심벌 참조를 직접 참조로 대체하는 과정
      • *심벌 참조: 클래스 파일에서 CONSTANT_Class_info, CONSTANT_Fieldref_info, CONSTANT_Methodref_info 등
    • 주로 7가지 타입의 심벌 참조에 대해 수행함 (클래스/인터페이스, 필드, 클래스 메서드, 인터페이스 메서드, 메서드 타입, 메서드 핸들, 호출 사이트 지정자)
    • <자바 가상 머신 명세>는 해석 단계를 수행하는 시간을 특정하지 않고, 그 대신 심벌 참조를 다루는 바이트코드 명령어들에 대해 실행하도록 규정
      • 따라서 클래스를 로드할 때 심벌 참조를 언제 해석할 지는 가상 머신이 결정
        • 상수 풀에 있는 심벌 참조를 바로 해석할지 또는 심벌 참조가 실제로 사용될 때까지 기다릴지
      • 심벌 참조를 다루는 바이트코드 명령어는 총 17개
        • anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, ldc2_w, multianewarray, new putfield, putstatic
    • 규칙
      • 동일한 심벌 참조에 대해 해석 요청이 여러 번 이루어지므로, 가상 머신은 첫 번째 해석 결과를 캐시
        • 실행 중에 상수 풀의 내용을 직접 참조했다면, 그 상수는 '해석되었다'고 표시하여 같은 해석을 반복하지 않도록 함
      • 해석을 반복해 수행하더라도 JVM은 같은 대상에 대해서는 항상 같은 결과를 내야
        • 특정 심벌 참조에 대한 처음 해석이 성공하면 다음번 요청도 반드시 성공해야 하고, 처음 해석이 실패했다면 다른 코드에서 요청한 해석 역시 실패해야 함(해당 심벌이 나중에 JVM 메모리에 성공적으로 로드되었어도)
      • 주의) invokedynamic 명령어에는 해당 규칙들이 적용되지 않음
        • 즉, invokedynamic에 의해 해석된 심벌 참조를 다른 invokedynamic이 다시 요청하면 이전 해석과는 결과가 다를 수 있음
        • invokedynamic의 목적은 원래 동적 언어 지원이므로 이 명령어가 사용하는 참조는 '동적으로 계산된 호출 사이트 지정자(dynamically computed call site specifier)'라고 함
        • 여기서 '동적'은 프로그램이 이 명령어를 실행할 때까지 해석을 수행할 수 없다는 뜻

     

    추가) 해석 단계에서 이야기하는 심벌 참조와 직접 참조

    더보기

    심벌 참조

    • 몇 가지 심벌로 참조 대상을 설명한다.
    • 여기서 심벌은 대상을 명확하게 지칭하는 데 이용될 수 있는 모든 형태의 리터럴이 될 수 있다.
    • 심벌 참조는 가상 머신이 구현한 메모리 레이아웃과 관련이 없고, 참조 대상이 반드시 가상 머신의 메모리에 로드되어 있을 필요도 없다.
    • 메모리 레이아웃은 가상 머신 구현에 따라 달라질 수 있지만, 심벌 참조는 달라지지 않는다.
      • 심벌 참조에 쓰일 수 있는 리터럴 형태는 <자바 가상 머신 명세>의 클래스 파일 구조에 명확하게 정의되어 있기 때문

    직접 참조

    • 포인터, 상대적 위치(오프셋) 또는 대상의 위치를 간접적으로 가리키는 핸들
    • 가상 머신에 구현된 메모리 레이아웃과 밀접하게 관련된다.
    • 똑같은 심벌 참조로부터 변환했어도 가상 머신에 따라 직접 참조는 달라질 수 있다.
    • 참조 대상이 가상 머신의 메모리에 이미 로드되어 존재해야 한다.

     

    5) 초기화

    • 클래스 로딩의 마지막 단계
    • JVM이 드디어 사용자 클래스에 작성된 자바 프로그램 코드를 실행하기 시작함
      • 준비 단계에서는 모든 변수(클래스 변수 해당, final 키워드로 정의된 상수 제외)에 시스템이 정의한 초기값인 0(데이터 타입별 제로값)을 할당함
      • 초기화 단계에서는 클래스 변수와 기타 자원을 프로그램 코드에 기술한 대로 초기화
    • 즉, 초기화 단계란 클래스 생성자인 <clinit>() 메서드를 실행하는 단계
      • *해당 메서드는 자바 컴파일러가 자동으로 생성하는 메서드
    • <clinit>() 생성 방식 및 프로그램 동작에 영향을 주는 방식
      • 모든 클래스 변수 할당과 정적 문장 블록(static {})의 내용을 취합하여 컴파일러가 자동으로 생성한다.
        • 컴파일러가 수집하는 순서는 문장이 소스 파일에 등장하는 순서에 영향을 받음
        • 따라서 정적 문장 블록에서는 정적 문장 블록보다 먼저 정의된 변수에만 접근할 수 있음
        • 단, 나중에 정의된 변수라도 정적 문장 블록에서 값을 할당하는 것은 가능함
      • 자바 언어에서 말하는 클래스의 생성자(<init>())와는 다르다.
        • 부모 클래스의 생성자를 명시적으로 호출하지 않아도 JVM은 하위 클래스의 <clinit>()가 실행되기 전에 부모 클래스의 <clinit>()부터 실행함
        • JVM이 실행하는 첫 번째 <clinit>()는 java.lang.Object의 <clinit>()임
      • 부모 클래스의 <clinit>()가 먼저 실행되므로 자연스럽게 부모 클래스의 정의된 정적 문장 블록이 자식 클래스의 변수 할당 연산자보다 먼저 실행된다.
      • <clinit>()가 클래스나 인터페이스에 반드시 필요한 것은 아니다.
        • 정적 문장 블록이 없거나 정적 변수에 값을 할당하지 않는 클래스라면 컴파일러가 해당 메서드를 생성하지 않을 수 있음
      • 인터페이스도 <clinit>() 메서드가 있을 수 있다. (정적 문장 블록은 사용할 수 없지만, 정적 변수에 초기값 할당은 가능)
        • 부모 인터페이스의 <clinit>() 메서드를 먼저 실행할 필요 없음
        • 부모 인터페이스는 부모 인터페이스가 실제로 사용되는 시점에 비로소 초기화 됨
        • 클래스를 초기화할 때도 클래스가 구현한 인터페이스의 <clinit>()를 실행하지 않음
      • JVM은 클래스의 <clinit>()가 멀티스레드 환경에서 적절히 동기화되록 해야 한다.
        • 여러 스레드가 한 클래스를 동시에 초기화하려 시도하면, 그중 한 스레드만 <clinit>()를 실행하고, 다른 스레드는 모두 대기하게 됨
        • 따라서 클래스의 <clinit>()에 시간이 오래 걸리는 작업이 포함되어 있다면 여러 스레드가 장기간 블록될 수 있음
          • 블록된 스레드는 <clinit>()를 실행하는 스레드가 실행을 끝마치면 깨어나는데, 깨어난 이후에는 <clinit>()로 진입하지 않고 건너뜀. 이런 식으로 같은 클래스 로더하에서 한 클래스는 오직 한 번만 초기화 됨

     

    클래스 로더

    • JVM 설계진은 필요한 클래스를 얻는 방법을 어플리케이션이 정할 수 있기를 원했음
    • 따라서 클래스 로딩 단계 중 '완전한 이름을 보고 해당 클래스를 정의하는 바이너리 바이트 스트림 가져오기'를 가상 머신 외부에서 수행하도록 했음
    • 이 역할을 맡은 코드를 '클래스 로더'라고 함

     

    1. 클래스와 클래스 로더

    • 각 클래스 로더는 독립적인 클래스 이름 공간을 지니기 때문에 클래스 로더를 빼놓고는 특정 클래스가 JVM에서 유일한지 판단할 수 없음
    • 두 클래스가 '동치인가' 여부는 두 클래스 모두 같은 클래스 로더로 로드했을 때만 의미가 있음
      • 여기서 말하는 '동치인가'는 해당 클래스 객체의 equals(), isAssignableFrom(), isInstance() 메서드의 반환값 또는 instanceof 키워드로 객체들의 관계를 결정하는 상황과 관련 있음
    • 서로 다른 클래스 로더로 읽어 들였다면, 비록 같은 가상 머신이고 똑같은 클래스 파일로부터 로드했더라도 다른 클래스로 인식

     

    예시) 다른 클래스 로더를 사용했을 때 instanceof 키워드의 동작 결과

    더보기
    package org.fenixsoft.jvm.chapter7;
    
    import java.io.IOException;
    import java.io.InputStream;
    
    /**
     * 클래스로더가 instanceof 키워드 동작에 미치는 영향
     *
     * @author zzm
     */
    public class ClassLoaderTest {
        public static void main(String[] args) throws Exception {
            // 1) 고유한 클래스 로더 생성
            ClassLoader myLoader = new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    try {
                        String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                        InputStream is = getClass().getResourceAsStream(fileName);
                        if (is == null) {
                            return super.loadClass(name);
                        }
                        byte[] b = new byte[is.available()];
                        is.read(b);
                        return defineClass(name, b, 0, b.length);
                    } catch (IOException e) {
                        throw new ClassNotFoundException(name);
                    }
                }
            };
    
            // 2) 방금 만든 클래스 로더로 현재 클래스(ClassLoaderTest)의 인스턴스 생성
            Object obj = myLoader.loadClass("org.fenixsoft.jvm.chapter7.ClassLoaderTest").newInstance();
    
            // 3) 실행 결과
            System.out.println(obj.getClass()); // org.fenixsoft.jvm.chapter7.ClassLoaderTest
            System.out.println(obj instanceof org.fenixsoft.jvm.chapter7.ClassLoaderTest); // false
        }
    }

     

    • 1) 간단한 클래스 로더를 직접 구현한다. 자신과 같은 경로에 있는 클래스 파일을 로드할 수 있는 클래스 로더이다.
    • 2) 이 로더를 이용해 이름이 "org.fenixsoft.jvm.chapter7.ClassLoaderTest"인 클래스를 로드하고 객체 인스턴스를 생성한다.
    • 3) 실행 결과의 첫 번째 줄에서는 이 객체가 실제로 org.fenixsoft.jvm.chapter7.ClassLoaderTest 클래스를 인스턴스화한 것임을 알 수 있다. 그러나 두 번째 줄에서는 org.fenixsoft.jvm.chapter7.ClassLoaderTest의 인스턴스가 아니라고 한다.
      • 이유)
        • JVM에 ClassLoaderTest 클래스가 두 개 존재하기 때문
        • 하나는 JVM의 어플리케이션 클래스 로더가 로드한 것, 다른 하나는 직접 구현한 사용자 정의 클래스 로더로 로드한 것
        • 둘 다 동일한 클래스 파일로부터 만들어졌지만 JVM은 별개의 클래스로 봄
        • JVM 입장에서는 서로 다른 타입을 비교한 것이므로 false를 반환한 것임

     

    2. 부모 위임 모델

    JVM 관점

    • 클래스 로더의 종류는 다음과 같이 두 가지뿐
      • JVM 자체의 일부인 부트스트랩 클래스 로더
        • 핫스팟 가상 머신에서는 C++로 구현함
      • 그 외 모든 클래스 로더
        • 추상 클래스인 java.lang.ClassLoader를 상속하여 자바로 구현하며, 가상 머신 외부에 독립적으로 존재함

     

    자바 개발자 관점

    • JDK 1.2부터 8까지 3계층 클래스 로더인 부모 위임 클래스 로딩 아키텍처를 유지해옴
    • 이 시기 자바 어플리케이션은 대부분 시스템이 제공하는 다음 세 가지 클래스 로더를 통해 로드되었음
      • 부트스트랩 클래스 로더
        • JAVA_HOME/lib 디렉토리나 -Xbootclasspath 매개 변수로 지정한 경로에 위치한 파일들과 JVM이 클래스 라이브러리로 인식하는 파일들(rt.jar, tools.jar 같은 파일을 말하며 파일 이름으로 식별함)을 로드
        • 자바 프로그램에서 직접 참조할 수 없으므로, 커스텀 클래스 로더 작성 시 로딩을 부트스트랩 클래스 로더에 위임하고자 할 때는 참조 대신 null을 사용함
      • 확장 클래스 로더
        • sun.misc.Launcher$ExtClassLoader를 말하며 자바로 구현되어 있어 개발자 프로그램 안에서 직접 사용할 수 있음
        • JAVA_HOME/lib/ext 디렉토리 또는 java.ext.dirs 시스템 변수로 지정한 경로의 클래스 라이브러리들을 로드
      • 어플리케이션 클래스 로더
        • sun.misc.Launcher$AppClassLoader를 말함
        • ClassLoader 클래스의 getSystemClassLoader() 메서드가 반환하는 클래스 로더라는 의미에서 '시스템 클래스 로더'라고도 함
        • classpath상의 클래스 라이브러리들을 로드하며, 개발자가 자바 코드에서 직접 사용할 수 있음
        • 어플리케이션에서 클래스 로더를 따로 만들어 사용하지 않는 경우 이 로더가 기본 클래스 로더가 됨
      • 필요시 사용자가 직접 만든 클래스 로더를 추가할 수 있음

     

    추가) 클래스 로더 부모 위임 모델

    더보기
    • 위와 같은 클래스 로더 간 계층 관계를 클래스 로더들의 부모 위임 모델이라고 함
      • 최상위 부트스트랩 클래스 로더 외에는 모두 부모가 있어야 함
      • 부모-자식 관계는 상속보다는 컴포지션 관계로 구현하며 부모 로더의 코드를 재사용함
      • 참고) 위와 같은 계층 관계는 필수가 아니라 자바 설계자들이 개발자들에게 권장하는 모범 사례임
    • 부모 위임 모델의 동작 방식
      • 모든 로드 요청은 우선 최상위인 부트스트랩 클래스 로더로 넘겨진다.
        • 클래스 로딩을 요청받은 클래스 로더는 처음부터 클래스 자체를 로드하려 시도하지 않음
        • 대신 수준에 맞는 상위 클래스 로더로 요청을 위임함
      • 상위 로더가 자신이 처리할 요청이 아니라고 판단하면(요청 받은 클래스가 자신의 검색 범위에 없다면) 비로소 하위 로더가 시도한다.
    • 장점
      • 클래스 로더를 부모 위임 모델로 구성하면 자바 클래스들이 자연스럽게 클래스 로더의 계층 구조를 따르게 됨
        • ex) rt.jar에 포함된 java.lang.Object 클래스의 로딩은 어떤 클래스 로더에 요청하더라도 최상위인 부트스트랩 클래스 로더가 처리
          • 즉, 프로그램이 아무리 많은 클래스 로더를 활용하더라도 Object 클래스는 모두 동일한 클래스임이 보장됨(항상 동일한 클래스 로더가 로딩하니까)

     

    참고) 부모 위임 모델의 구현

    더보기
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                        // 부모 클래스 로더가 ClassNotFoundException을 던진다는 것은 부모 클래스 로더는 요청을 처리할 수 없다는 뜻
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        
                        // 부모 클래스 로더가 실패하면 자신의 findClass() 메서드를 호출해 직접 시도함
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    • 요청받은 클래스가 이미 로드되어 있는지 확인하고, 아직이라면 부모 로더의 loadClass를 호출함
    • 부모 로더가 null이면 부트스트랩 클래스 로더를 부모로 사용함
    • 부모 로더가 로드에 실패하여 ClassNotFoundException을 던지면 비로소 자신의 findClass()를 호출함

     

    자바 모듈 시스템

    • JDK 9에 도입된 모듈 시스템(JPMS)은 자바 기술에 있어 중요한 개선
    • 모듈화라는 '자유롭게 설정 가능한 캡슐화 격리 메커니즘'을 만들기 위해 JVM은 클래스 로딩 아키텍처를 적절히 변형했음
    • 자바 모듈 정의에는 코드 외에도 다음 내용이 포함됨
      • requires: 다른 모듈에 대한 의존성 목록
      • exports: 다른 모듈에서 사용할 수 있는 패키지 목록
      • open: 다른 모듈에서 리플렉션 API로 접근할 수 있는 패키지
      • uses: 현재 모듈이 사용할 서비스 목록
      • provides: 다른 모듈에 제공하는 서비스 목록
    • 특징
      • JDK 9부터는 모듈이 의존하는 다른 모듈들을 명시할 수 있어, 필요한 의존성이 모두 갖춰졌는지 어플리케이션 개발 단계에서 확인할 수 있음
      • 자바 모듈은 public 타입 중에서도 외부 모듈에 공개할 타입을 따로 명시하여 접근 권한을 더 세분화해 관리함
        • 이러한 접근 통제는 주로 클래스 로딩 과정 > 해석 단계에서 이루어짐

     

    모듈화 시대의 클래스 로더는 어떻게 바뀌었을까?

    • 3계층 클래스 로더 아키텍처와 부모 위임 모델의 근간은 바뀌지 않았지만 주목할 만한 주요 변경점이 존재함
      1. 확장 클래스 로더가 플랫폼 클래스 로더로 대체되었다.
        • JDK 전체가 모듈화되면서 모듈들 안의 클래스 라이브러리들은 자연스럽게 확장성 요구 사항을 충족하게 되었음
        • 그 결과 JDK의 기능을 확장하기 위해 쓰이던 JAVA_HOME/lib/ext 디렉토리나 java.ext.dirs 시스템 변수를 사용하는 이전 방식을 유지할 필요가 없어짐
      2. 플랫폼 클래스 로더와 어플리케이션 클래스 로더가 더는 java.net.URLClassLoader로부터 파생되지 않는다.
        • 모두 jdk.internal.loader.BuiltinClassLoader에서 파생
        • 새로운 모듈식 아키텍처에서는 모듈에서 클래스를 로드하는 방법과 모듈 간 접근 처리 로직이 BuiltinClassLoader에 구현되어 있음
      3. 클래스 로더와 부모 위임 모델은 여전히 유지하지만 클래스 로딩의 위임 관계에 변화를 주었다.
        • 클래스 로딩을 요청받은 플랫폼 및 어플리케이션 클래스 로더는 부모 로더에 위임하기 전에 해당 클래스가 특정 시스템 모듈에 속하는지 확인
        • 특정 시스템 모듈에 속한다면 (부모 로더가 아닌) 해당 모듈을 담당하는 로더에 위임

     

    추가) 클래스 로더 상속 구조 변화

    더보기
    • BuiltinClassLoader의 하위에 존재
    • 부트스트랩 클래스 로더를 자바 클래스로 구현
      • 이전까지는 C++로 따로 구현하여 자바 클래스 형태로는 존재하지 않았음
      • 이전 코드 하위호환을 위해 Object.class.getClassLoader()처럼 부트스트랩 클래스 로더를 얻는 모든 시나리오에서는 여전히 BootClassLoader 객체가 아닌 null을 반환함

     

    추가) JDK 9 이후의 클래스 로더 위임 방식

    더보기
    • 자바 모듈 시스템에서 세 가지 클래스 로더는 각각 다음 모듈을 담당함

    댓글

Designed by Tistory.