클래스의 생명주기는 클래스가 로드되어 사용되고 최종적으로 언로드되는 전체 과정을 설명합니다. 이 과정은 다음과 같은 단계들로 나뉩니다:
- 로드
- 연결 (검증, 준비, 해결 세 가지 하위 단계 포함)
- 초기화
- 사용
- 언로드
1. 로드 단계
- 로드(Loading) 단계에서는 클래스 로더가 클래스의 완전한 이름을 기반으로 다양한 경로를 통해 바이트코드 정보를 이진 스트림 방식으로 가져옵니다. 프로그래머는 이를 확장하여 Java 코드에서 다른 경로를 제공할 수 있습니다.
- 로컬 디스크에서 파일을 가져오기
- Spring과 같은 프레임워크에서 실행 중에 동적 프록시를 생성하기
- 네트워크를 통해 Applet 기술을 사용해 바이트코드 파일을 가져오기
-
클래스 로더가 클래스를 로드한 후, JVM은 메서드 영역에 바이트코드 정보를 저장하며, 여기에는 InstanceKlass 객체가 생성되며 클래스의 모든 정보와 다형성 등의 특정 기능을 구현하는 정보가 포함됩니다.
-
또한 JVM은 힙에서 메서드 영역의 데이터와 유사한
java.lang.Class객체를 생성합니다. 이 객체는 Java 코드에서 클래스의 정보를 얻고 정적 필드 데이터를 저장하는 데 사용됩니다(JDK8 이후).
2. 연결 단계
연결 단계는 세 가지 하위 단계로 나뉩니다:
- 검증: 내용이 Java Virtual Machine Specification을 충족하는지 확인.
- 준비: 정적 변수에 초기값 할당.
- 해결: 상수 풀 내의 심볼릭 참조를 직접 참조로 변환.
검증
검증의 주요 목적은 Java 바이트코드 파일이 JVM 사양의 제약 조건을 준수하는지 확인하는 것입니다. 일반적으로 이 단계는 프로그래머가 직접 참여하지 않으며 다음 네 부분으로 구성됩니다:
- 파일 형식 검증: 예를 들어 파일이 0xCAFEBABE로 시작하고 주차 버전 번호가 현재 JVM 버전 요구사항을 충족하는지 확인.
- 메타 정보 검증: 예를 들어 모든 클래스는 부모 클래스(super)를 가져야 합니다.
- 명령어 의미 검증: 예를 들어 메소드 내부에서 잘못된 위치로 점프하는지 확인.
- 심볼릭 참조 검증: 예를 들어 다른 클래스의 private 메소드에 접근하려는 시도를 막음.
준비
준비 단계에서는 정적 변수(static)에 대해 메모리를 할당하고 기본값을 설정합니다.
| 데이터 타입 | 초기값 |
|---|---|
| int | 0 |
| long | 0L |
| short | 0 |
| char | '\u0000' |
| byte | 0 |
| boolean | false |
| double | 0.0 |
| 참조 타입 | null |
다음 코드를 보겠습니다:
public class Student {
public static int value = 1;
}
준비 단계에서는 value에 메모리를 할당하고 기본값 0으로 설정하며, 초기화 단계에서 값이 1로 변경됩니다.
final로 선언된 기본 데이터 타입의 정적 변수는 준비 단계에서 바로 값을 할당합니다.
예를 들어:
public class Example {
public static final int value = 1;
}
여기서 final로 선언된 변수는 준비 단계에서 값이 1로 설정됩니다.
해결
해결 단계에서는 상수 풀의 심볼릭 참조를 직접 참조로 대체합니다. 심볼릭 참조는 바이트코드 파일에서 번호를 사용해 상수 풀의 내용을 참조하는 것입니다. 직접 참조는 메모리 주소를 사용해 데이터를 접근하는 방식입니다.
3. 초기화 단계
초기화 단계에서는 clinit(class initialization) 메소드의 바이트코드 명령문이 실행됩니다. 이는 정적 코드 블록의 코드를 포함하며 정적 변수에 값을 할당합니다.
다음 코드를 보겠습니다:
public class Demo {
public static int value = 1;
static {
value = 2;
}
public static void main(String[] args) {
}
}
컴파일된 바이트코드 파일에는 세 가지 메소드가 생성됩니다:
- init 메소드: 객체 초기화 시 실행
- main 메소드: 프로그램 시작점
- clinit 메소드: 클래스 초기화 단계에서 실행
clinit 메소드의 바이트코드 명령문은 다음과 같이 작동합니다:
iconst_1: 상수 1을 운영 스택에 넣습니다. 현재 스택에는 1만 존재합니다.putstatic: 운영 스택에서 숫자를 꺼내 힙의 정적 변수 위치에 넣습니다. #2는 상수 풀의 정적 변수value를 가리킵니다. 해결 단계에서 이는 변수의 주소로 대체됩니다.- 나머지 두 단계는 비슷하게 진행되어
value가 2로 설정됩니다.
코드 위치를 바꿔보겠습니다:
public class Demo {
static { value = 2; }
public static int value = 1;
public static void main(String[] args) {}
}
바이트코드 명령문의 위치도 바뀌며 초기화가 끝난 후 value의 최종 값은 1이 됩니다.
클래스 초기화를 트리거하는 몇 가지 방법:
- 클래스의 정적 변수나 정적 메소드에 접근할 때 (단, final로 선언된 변수이고 등호 우측이 상수인 경우는 초기화되지 않습니다).
Class.forName(String className)호출.- 해당 클래스의 객체 생성 시 (
new연산자 사용). - Main 메소드가 실행되는 클래스.
JVM 옵션 -XX:+TraceClassLoading을 추가하면 로드 및 초기화된 클래스를 출력할 수 있습니다.
clinit 메소드가 실행되지 않는 경우:
- 정적 코드 블록이나 정적 변수 초기화 문이 없는 경우.
- 정적 변수가 선언되었지만 초기화 문이 없는 경우.
- final로 선언된 정적 변수는 준비 단계에서 초기화됩니다.
- 배열 생성은 배열 요소의 클래스 초기화를 유발하지 않습니다.