이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › JVM
클래스 로딩과 초기화
클래스의 static 블록은 언제 실행되나
클래스에 적어 둔 static 초기화 블록은 프로그램이 시작될 때 실행되는 것이 아니라, 그 클래스가 처음으로 "제대로" 쓰일 때 한 번 실행됩니다. 그런데 클래스 이름이 코드에 나온다고 해서 모두 그런 사용은 아닙니다.
실행으로 대조 · JDK 21에서 열여덟 가지 조합을 새로 컴파일하고 실행해 대조
이 실습에 나오는 개념
- 클래스 로더
- 클래스 파일을 찾아 메모리로 읽어 들이는 JVM의 구성 요소다. 클래스는 프로그램 시작 때 전부 읽는 것이 아니라 처음 필요해지는 시점에 로딩된다. 읽어 들인 클래스는 JVM이 링킹(검증, 준비, 그리고 심볼릭 참조의 해석)과 초기화(static 초기화 실행)를 거쳐 사용할 수 있게 만든다.
- 클래스 초기화
- 클래스의 static 필드에 처음 값을 넣고 static 초기화 블록을 실행하는 단계다. 클래스를 읽어 들이는 로딩과는 다른 단계이며, 그 클래스의 객체를 만들거나 static 메서드를 부르거나 상수가 아닌 static 필드를 쓸 때처럼 클래스가 처음으로 실제 쓰이는 순간에 일어난다. 클래스 하나에 한 번만 일어나고, 자식 클래스를 초기화할 때는 부모 클래스가 먼저 초기화된다. 배열을 만들거나 클래스이름.class를 쓰는 것, 컴파일 시점 상수를 읽는 것은 초기화를 일으키지 않는다.
- static 초기화 블록
- 클래스 안에 static { ... } 모양으로 적는 코드 블록으로, 클래스가 초기화될 때 한 번 실행된다. static 필드에 넣을 값을 여러 줄의 코드로 계산해야 할 때 쓴다. static 필드의 초기화 식과 static 블록은 소스에 적힌 순서대로 위에서 아래로 실행된다. 객체를 만들 때마다 실행되는 생성자와 달리, 객체를 몇 개 만들든 다시 실행되지 않는다.
- 컴파일 시점 상수
- static final int MAX = 10처럼 final이고, 기본형이나 String 타입이며, 값이 리터럴 같은 상수식으로 정해지는 변수다. 컴파일러는 이런 상수를 쓰는 곳에 값을 직접 넣어 두므로, 실행할 때는 그 상수를 선언한 클래스를 찾아가지 않고 그 클래스의 초기화도 일어나지 않는다. final이더라도 메서드를 호출해야 값이 나오는 필드는 컴파일할 때 값을 알 수 없으므로 상수가 아니다. 값을 직접 넣어 두기 때문에, 상수의 값을 바꾸면 그것을 쓰는 코드도 다시 컴파일해야 새 값이 반영된다.
- static
- 인스턴스가 아니라 클래스에 속하는 멤버임을 나타내는 키워드다. static 필드는 클래스당 하나만 존재해 모든 인스턴스가 공유하고, static 메서드는 객체를 만들지 않고 호출한다. 값이 바뀌는 static 필드는 사실상 전역 변수이므로 여러 스레드가 동시에 접근할 때 문제가 되기 쉽다.
이렇게 바꿔 보세요
- 처음 설정으로 — 상수 읽기. static 블록이 실행되지 않고, 클래스를 읽어 들이지도 않습니다.
- new로 객체 만들기 — 처음 만들 때 한 번만 static 블록이 실행됩니다.
- static final인데 상수가 아닌 필드 — 실행해야 값이 정해지므로 초기화가 일어납니다.
- 배열만 만들면 — 클래스를 읽어 들이기만 하고 초기화하지 않습니다.
- 자식 클래스의 객체 만들기 — 부모가 먼저, 그다음 자식이 초기화됩니다.
- 부모의 static 필드를 자식 이름으로 — Child.count라고 썼지만 Child는 초기화되지 않습니다.
- 클래스이름.class 쓰기 — 클래스 정보만 가리킬 뿐 초기화하지 않습니다.
처음 설정으로 실행하면
- System.out.println("main 시작"): 프로그램이 시작됐습니다. Parent와 Child를 쓰는 코드를 아직 만나지 않았으므로, JVM은 두 클래스를 읽어 들이지도 않았고 static 블록도 실행하지 않았습니다.
- int a = Parent.MAX: MAX는 컴파일할 때 값이 정해지는 상수입니다. 컴파일러가 이 줄을 int a = 10;과 같게 바꿔 놓았기 때문에, 실행할 때는 Parent 클래스를 찾아갈 일이 없습니다. 클래스를 읽어 들이지 않고 static 블록도 실행되지 않습니다.
- System.out.println("첫 번째 사용 끝"): 여기까지 static 블록은 Parent에서 0번, Child에서 0번 실행됐습니다.
- int b = Parent.MAX: 이번에도 값 10을 바로 씁니다. 몇 번을 읽어도 클래스는 초기화되지 않습니다.
- System.out.println("두 번째 사용 끝"): 프로그램이 끝납니다. static 블록이 실행된 횟수는 Parent 0번, Child 0번입니다.
실행 결과
- static final이고 값이 숫자나 문자열 리터럴처럼 컴파일할 때 정해지는 필드는 "컴파일 시점 상수"입니다. 컴파일러가 쓰는 쪽 코드에 값을 직접 넣어 두므로, 실행할 때 그 클래스는 쓰이지 않습니다. 그래서 상수만 바꾸고 그 클래스만 다시 컴파일하면, 쓰는 쪽에는 옛 값이 남습니다.
코드
class Parent {
static final int MAX = 10; // 컴파일할 때 값이 정해지는 상수
static { System.out.println("Parent 초기화"); } // static 초기화 블록
}
class Child extends Parent {
static { System.out.println("Child 초기화"); }
}
System.out.println("main 시작");
int a = Parent.MAX;
System.out.println("첫 번째 사용 끝");
int b = Parent.MAX;
System.out.println("두 번째 사용 끝");
이 모형의 전제
열여덟 가지 조합마다 화면의 코드와 같은 소스를 이 PC의 JDK 21에서 새로 컴파일해 실행하고, 출력된 줄의 순서와 읽어 들인 클래스를 대조했습니다. 클래스를 읽어 들인 시점은 -Xlog:class+load 옵션으로 얻은 JVM의 기록과 대조했습니다. 초기화가 언제 일어나는지는 자바 언어 명세가 정해 둔 것이라 어느 JVM에서나 같습니다. 반면 클래스를 언제 읽어 들이는지는 JVM 구현에 맡겨져 있어서, "처음 쓰일 때 읽는다"와 "상수만 읽으면 읽지도 않는다"는 이 PC의 JVM(HotSpot)에서 확인한 동작입니다. 상수 읽기가 값 10으로 바뀌어 컴파일된다는 것은 javap로 바이트코드를 보고 확인했습니다. 읽어 들인 뒤 초기화 전에 거치는 검증, 준비 같은 세부 단계와, Class.forName이나 인터페이스의 초기화 규칙은 다루지 않았습니다.
앞 실습: JIT 워밍업 · 다음 실습: 입출력 스트림과 버퍼