ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JVM 클래스 로딩 과정 알아보기
    Java 2025. 1. 9. 03:50

    JVM 클래스 로딩 과정은 어떤 식으로 진행되는 지 살펴보자

    Goal

    • JVM 의 클래스 로딩 과정을 알아보며 동작 원리를 이해하기
    • 얕게 알고 있던 동작 과정을 정확히 설명할 수 있는 수준으로 이해하기

    클래스 로딩 과정

    로딩

    • 클래스로더가 클래스의 이름으로 바이트 코드를 찾고 해당 바이트 코드의 내용을 읽어 바이트 스트림으로 가져온다.
      • 다양한 위치에서 바이트 코드를 읽어올 수 있다. (jar, 네트워크, 동적 생성 등)
    • 바이트 코드를 JVM 메모리의 Method 영역에 런타임 데이터 구조로 변환하여 저장한다.
    • Method 영역에 저장을 한 뒤java.lang.Class 객체를 생성하여 힙 메모리에 저장한다.
      • 힙 메모리에 저장된 이 Class<?> 타입을 통해 리플렉션을 사용할 수 있다.

    링크

    링크는 검증(Verify), 준비(Prepare), 해석(Resolve) 세단계를 거쳐 로딩된 바이트 코드를 검증하고 바이트 코드간 심볼릭 참조를 실제 메모리 참조로 변환하는 작업을 처리한다.

    검증(Verify)

    로딩된 바이트 코드는 런타임에 참조 가능한 데이터 구조로 Method 영역에 저장된다.
    검증 단계에서는 저장된 데이터에서 다음을 검증한다. 에러가 발생하면 VerifyError 를 던진다.

    1. 파일 형식 검증
    • Method 영역의 데이터가 자바 가상 머신 명세의 제약에 만족하는 지를 검증
    • 예 : 매직 넘버가 0xCAFEBABE 가 맞는지, 상수 풀 태그 정보가 존재하는 지 등등
    1. 메타데이터 검증
    • 자바 언어 명세의 규칙을 만족하는 지를 검증
    • 상속 규칙, 오버로딩, 오버라이딩 규칙, 추상 클래스 규칙 등등
    1. 바이트코드 검증
    • 바이트 코드에서 실행되는 실제 코드는 Code Attribute 에 저장된다.
    • 바이트 코드 검증 단계에서는 Code Attribute 에 저장된 메서드 본문을 검증하는 단계이다.
    • 피연산자 스택, opcode 타입 검증
      • 피연산자 스택에는 int 타입이 있는데, opcode 는 지역 변수 슬롯에는 long 타입으로 저장하는 경우가 없도록 해야한다.
      • 검증 과정에서 모든 바이트 코드를 분석하는 것은 오버헤드가 큰 작업이다.
      • JVM 은 StackMapTable 을 바이트코드에 추가하여 opcode 의 오프셋에 로컬 변수 상태와 피연산자 스택의 타입을 제공한다.
      • 검증 단계에서 JVM은 바이트코드의 StackMapTable 을 참조하여 유효성을 검증하기만 하면된다.
    • 점프 명령어가 유효한 위치를 가리키는 지 검증
      • 점프하는 주소가 메서드 본문 밖의 바이트 코드 명령어를 가리킬 수 없다.
    • 형변환 타입 검증
    1. 심볼릭 참조 검증
      링크의 마지막 단계인 해석 단계에서는 심볼릭 참조를 실제 메모리 주소로 변환하게 되는데, 이때 일어나는 검증.
      이 과정에서 발생하는 에러는 IncompatibleClassChangeError 의 하위 에러를 던진다.
    • 클래스에서 참조하는 외부 클래스, 메서드, 필드에 접근 가능한 지를 검증
      • FQCN에 해당하는 클래스를 정보를 Method 영역에서 찾는다.
      • 존재하는 클래스, 메서드, 필드인 지를 검증
      • 접근제어자를 검증

    준비(Prepare)

    준비 단계에서는 클래스에 선언된 정적 변수들에 초기값을 설정한다.
    초기값을 설정한다는 것이지 초기화를 하는 것이 아니다.

    예를 들어 다음과 같은 코드가 있다고 가정하자.

    public static int value = 1;
    • 위의 VALUE 변수는 int 형 데이터 타입의 변수이다. 이 변수에 아무런 값을 할당하지 않았을 경우에 값은 0이다.
    • 준비 단계에서 설정하는 초기값은 데이터 타입에 맞는 값을 설정하는 것이다.

    하지만 바이트 코드의 필드 테이블(field_info)의 속성 테이블에 ConstantValue 속성이 존재한다면 해당 정적 변수는 초기화 단계가 아니라 준비 단계에서 값을 초기화 하게 된다.

    public class Test {
      public static int value = 1; // 일반 전역 변수
      public static final int CONSTANT_VALUE = 1; // final 을 붙인 전역 변수
    }

    위의 코드를 컴파일하고, 바이트 코드를 javap 명령어로 디컴파일한 결과를 보자

     

     

    • 일반 전역 변수는 ConstantValue 속성이 없다.
    • final 을 붙인 전역 변수는 ConstantValue 속성이 생겼다.
    • ConstantValue 속성이 붙은 전역 변수는 준비 단계에서 초기화가 이루어진다.

    해석(Resolve)

    자바는 C, C++ 과 달리 링크를 컴파일 시점이 아닌 클래스 로딩 시점에 처리한다.
    따라서 바이트 코드 내의 참조는 실제 메모리 주소를 참조하는 것이 아닌 심볼릭 참조(Symbolic Reference)다.
    심볼릭 참조는 바이트 코드의 구조에서 살펴본 것 처럼 상수 풀의 정보를 참조하고 있다.

    예 : CONSTANT_Class_info, CONSTANT_Method_info 등등

    해석 단계에서는 심볼릭 참조를 직접 참조로 대체하는 과정이다.

     

    예시 코드를 살펴보자.

    public class MyClass {
      public void fun() {
        System.out.println("Hello World");
      }
    
    }
    
    public class Test {
      public static void main(String[] args) {
        MyClass myClass = new MyClass(); // CONSTANT_Class_info 해석
        myClass.fun(); // CONSTANT_Method_info 해석
      }
    }

    MyClass, Test 클래스가 있다.
    Test 의 main 메서드에서 MyClass 인스턴스를 생성할 때 발생할 수 있는 상황은 두가지가 있다.

    • 해석된 MyClass 심볼릭 참조가 있는 경우. (해석된 경우는 해석된 참조를 사용하면 된다.)
    • 해석된 MyClass 심볼릭 참조가 없는 경우

    MyClass 심볼릭 참조 해석되지 않았다면 클래스 로더를 호출하여 앞에서의 과정처럼 MyClass 의 클래스를 로딩-링킹-초기화 작업을 거쳐 JVM 메모리에 로드한다.
    MyClass 가 메모리에 로드되면 Test 가 가리키는 MyClass 심볼릭 참조를 직접 참조로 대체한다.

    메서드를 호출하는 경우에도 위와 비슷한 과정을 거치게 되는데, 추가적으로 접근제어자를 검증한다.


    초기화

    • 초기화 단계에서는 전역 변수의 초기화, static 블록 실행을 수행한다.
    • 이 초기화 과정은 클래스 생성자 메서드인 <clinit> 메서드를 통해서 이루어진다.

    몇가지의 상황을 살펴보며 <clinit> 이 호출되는 상황을 알아보자.

    IntelliJ 의 ASM Bytecode Viewer Plugin 을 사용해서 살펴보았다.

    1. static 변수에 값이 할당되는 경우
      public class Test {
      public static int value = 1;
      }

    중요한 부분은 마지막 부분인 <clinit> 이다.
    static 변수에 값을 할당하는 작업은 <clinit> 을 생성한다.

    1. static 블록이 있는 경우
    public class Test {
      public static int value;
    
      static {
        System.out.println("Hello");
      }
    }

    • static 블록이 있는 경우도 <clinit> 을 생성한다.
    1. static 변수에 할당 X, static 블록 X 
    public class Test {
      public static int value;
    }

    • static 변수 초기화가 필요 없고, static 블록이 없다면 <clinit> 을 생성하지 않는다.

    'Java' 카테고리의 다른 글

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