JVM 기반 어플리케이션의 처음 실행 단게인 Class 로딩 단계를 조금 더 깊게 파헤쳐본다.
- 클래스를 설명하는 데이터를 클래스 파일로부터 메모리로 읽어 들인다
- 그 데이터를 검증, 변환, 초기화한다
- 최종적으로 가상 머신이 곧바로 사용할 수 있는 자바 타입을 생성한다.
자바의 동적 확장 언어 기능을 제공할 수 있는 것은 런타임에 이루어지는 동적로딩과 동적 링킹 덕분이다. (애플리케이션을 인터페이스 중심으로 작성해두면 실제 구현 클래스를 결정하는 일은 실행 시까지 미룰 수 있다.)
가상 머신의 메모리 로드 과정을 더 간략하게 표현하면은 아래 과정을 거친다고 한다. 그리고 검증, 준비, 해석 단계를 묶어 링킹이라고 한다.
로드 -> 검증 -> 준비 -> 해석 -> 초기화 -> 사용 -> 언로딩
클래스 로딩 처리 과정에 첫번째 단계인 로딩 과정이다.
책에서는 말이 어렵게 서술 되었는데 조금 풀어서 설명하자면 (ChatGPT 의 도움과 함께)
클래스 파일 (.class) 가져오기. 완전한 이름이란? 패키지를 포함한 클래스 이름으로서 예를 들면 com.example.MyClass 라고 하면 JVM 은 com/example/MyClass.class 파일을 찾는다.
클래스 파일을 메모리로 읽어오기. 클래스를 찾으면 바이트 코드로 읽어와야한다. .class 파일은 바이너리 데이트로 되어있기 때문에 JVM 은 이 데이터를 메서드 영역 (Method Area) 에 저장할 런타임 데이터 구조로 변환한다.
Class 객체 생성 (힙 메모리) 클래스를 메모리에 올리고 나면 java.lang.Class 객체를 힙 영역에 생성하고 이 객체를 통해서 메서드 영역의 클래스 정보를 참조할 수 있다.
로딩 단계가 끝나면 자바 가상머신이 정의한 형식에 맞게 메서드 영역에 저장된다. 로딩 단계와 링킹 단계의 일부 동작 (예: 바이트코드 파일 형식을 검증하는 동작 중 일부) 는 서로 중첩되어 진행된다.
검증은 링킹 과정 중 첫번째 단계다. 검증 단계는 매우 중요한 단계이고 JVM 을 악성코드로부터 보호해준다. 검증은 아래 4단계로 크게 나뉘어 질 수 있다.
준비는 static (정적) 변수를 메모리에 할당하고 초기값을 설정하는 단계다. 인스턴스 변수의 경우 실제 객체가 Heap 영역에 할당이 될 떄 생성되는것이기 때문에 클래스 변수만 이 범위에 해당된다. 또한, 준비 단계에서 클래스 변수에 해당하는 초기값은 해당 데이터 타입의 제로 값이다.
public static int value = 123;
의 경우, 실제 준비 단계를 마친 이후 value 값은 123이 아닌 0이다. 준비 단계에서는 어떠한 자바 메서드도 아직 실행되지 않은 상태이기 때문이다. 123이 실제로 할당되는 일은 '클래스 초기화 단계" 를 가서야 이루어진다.
하지만 예외의 경우는 존재하는데, 클래스 필드에 ConstantValue 속성이 존재한다면 지정한 값을 할당한다.
public static final int value = 123;
이 코드를 컴파일 하면 javac 가 value 변수를 위한 ConstantValue 속성을 생성한다. 그렇다면 준비 단계 때 123의 값이 value에 할당된다.
해석은 자바 가상 머신이 상수 풀의 심벌 참조를 직접 참조로 대체하는 과정이다.
음 한마디로 reference 로 남아있는 변수를 실제 객체의 메모리 주소로 할당하는 느낌?
초기화는 클래스 로딩의 마지막 단계며, 사용자 클래스에 정의된 자바 프로그램 코드를 실행한다. 앞서 static 예시에서 준비 단계에서는 모든 변수에 시스템이 정의한 초기값인 0을 할당했다. 초기화 단계에서는 우리가 코드에서 설정한 값을 할당하고, static {} 블록을 실해하는 단계다.
더 직관적으로, 초기화 단계에서는 클래스 생성자인 cinit () 메서드를 실행한다고 한다. 컴파일러에 의해 자동으로 생성하는 메서드라 직접 작성할 수는 없다.
cinit 은 모든 클래스 변수 할당과 정적 문장 블록 (static) 내용을 취합하여 컴파일러가 자동으로 생성한다.
class Example {
static int number = 10;
static String text = "Hello";
static {
System.out.println("static 블록 실행!");
}
}
위에 작성 내용이 아래 처럼 하나의 문장 블록으로 실행된다고 보면된다.
static {
number = 10;
text = "Hello";
System.out.println("static 블록 실행!");
}
내 생각 : 준비 단계에서 static final 값을 할당하는 것과, 초기화 단계에서 static 값을 할당하는 것의 성능 차이를 조사해봤지만, 의미 있는 차이는 없다고 한다. 하지만 final 키워드를 사용하면 해당 값이 변경되지 않는 불변(Immutable) 속성을 가지므로, 멀티스레드 환경에서도 동기화 문제 없이 안전하게 사용할 수 있다.