English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Java 클래스 로드 전체 과정
한 개의 Java 파일이 로드되고 언로드되는 이 생명 과정에서, 총으로4단계:
로드->링크(검증+준비+해석)->초기화(사용 전의 준비)->사용->언로드
로드(사용자 정의 로드를 제외한)+링크 과정은 JVM이 전적으로 책임지며, 클래스를 초기화 작업을 수행해야 할 때(로드+링크는 이전에 완료되었습니다), JVM은 엄격한 규정(네 가지 상황)을 가지고 있습니다:
1. new, getstatic, putstatic, invokestatic 이렇게4클래스가 초기화되지 않았을 때, 바로 초기화 작업을 수행합니다. 실제로는3이 경우: 클래스를 new로 인스턴스화할 때, 클래스의 스태틱 필드를 읽거나 설정할 때(final로修饰된 스태틱 필드는 제외,因为他们已经被塞进常量池了)、스태틱 메서드를 실행할 때。
2. java.lang.reflect.*의 메서드를 통해 클래스에 반영을 가할 때, 클래스가 아직 초기화되지 않았다면 즉시 초기화합니다.
3. 클래스를 초기화할 때, 부모 클래스가 아직 초기화되지 않았다면, 먼저 부모 클래스를 초기화합니다.
4. JVM이 시작될 때, 사용자는 실행할 주 클래스를 지정해야 하며(static void main(String[] args)이 포함된 클래스),JVM은 이 클래스를 먼저 초기화합니다.
위와 같습니다4이러한 사전 처리는 클래스에 대한 적극적 참조로 간주되며, 이 외의 모든 상황은 비적극적 참조로 간주되며, 클래스 초기화를 유발하지 않습니다. 다음은 비적극적 참조의 예제도 포함됩니다:
/** * 비동기 참조 상황1 * 서브 클래스에서 부모 클래스의 스태틱 필드를 참조할 때, 서브 클래스의 초기화를 유발하지 않습니다 * @author volador * */ class SuperClass{ static{ System.out.println("super class init."); } public static int value=123; } class SubClass extends SuperClass{ static{ System.out.println("sub class init."); } } public class test{ public static void main(String[]args){ System.out.println(SubClass.value); } }
출력 결과: super class init.
/** * 비동기 참조 상황2 * 클래스를 배열 참조로 참조할 때, 이 클래스의 초기화를 유발하지 않습니다 * @author volador * */ public class test{ public static void main(String[] args){ SuperClass s_list=new SuperClass[10]; } }
출력 결과: 출력 없음
/** * 비동기 참조 상황3 * 상수는 컴파일 단계에서 호출된 클래스의 상수 풀에 저장되며, 실제로 상수 정의 클래스에 참조되지 않기 때문에 자연스럽게 초기화가 일어나지 않습니다 * @author root * */ class ConstClass{ static{ System.out.println("ConstClass init."); } public final static String value="hello"; } public class test{ public static void main(String[] args){ System.out.println(ConstClass.value); } }
출력 결과: hello(힌트: 컴파일 중 ConstClass.value가 hello 상수로 변환되어 test 클래스의 상수 풀에 넣혔습니다)
위는 클래스의 초기화에 대한 것이며, 인터페이스도 초기화되어야 합니다. 인터페이스의 초기화는 클래스의 초기화와 약간 다릅니다:
위의 코드는 static{}를 사용하여 초기화 정보를 출력합니다. 인터페이스는 할 수 없지만, 인터페이스가 초기화될 때 컴파일러는 인터페이스의 멤버 변수를 초기화하는 데 사용되는 <clinit>() 클래스 생성자를 생성합니다. 이 점은 클래스의 초기화에서도 동일하게 적용됩니다. 정작 차이는 세 번째 점에서, 클래스의 초기화는 부모 클래스가 모두 초기화되기 전에 초기화가 수행되어야 하지만, 인터페이스의 초기화는 부모 인터페이스의 초기화에 대해 크게 관심이 없다는 점입니다. 즉, 자식 인터페이스가 초기화될 때 부모 인터페이스의 초기화가 완료되지 않아도 됩니다. 실제로 부모 인터페이스가 사용될 때(예를 들어, 인터페이스 상의 상수를 참조할 때)에만 초기화됩니다.
하나의 클래스의 로드 전체 과정을 분석해 보겠습니다: 로드->검증->준비->해석->초기화
먼저 로드를 시작합니다:
이 부분에서 가상 기계가 완료해야 합니다3이 일은
1. 클래스의 전체 제한 이름을 통해 이 클래스를 정의하는 바이너리 바이트 스트림을 가져옵니다.
2. 이 바이트 스트림을 대표하는 정적 저장 구조를 메서드 영역의 실행 시 데이터 구조로 변환합니다.
3. 이 클래스를 대표하는 java.lang.Class 객체를 java 힙에서 생성하여 메서드 영역의 데이터에 접근하는 엔트리 포인트로 사용합니다.
첫 번째 점에 대해, 매우 유연합니다. 많은 기술은 여기서 접근합니다. 왜냐하면 이는 바이너리 스트림이 어디서 오는지에 제한을 두지 않기 때문입니다:
class 파일에서 옴->일반 파일 로드
zip 패키지에서 옴->jar 파일에 포함된 클래스 로드
네트워크에서 옴->Applet
..........
로드 과정의 다른 단계보다 로드 단계의 제어성이 가장 강합니다. 왜냐하면 클래스 로더는 시스템에서 사용할 수 있으며, 자신이 작성한 로더를 사용할 수 있기 때문에, 프로그래머는 바이트 스트림의 가져옴을 제어할 수 있는 방식을 자신의 방식으로 작성할 수 있습니다.
이진 스트림을 가져오는 작업이 완료되면 JVM이 필요한 방식으로 메서드 영역에 저장되며, 동시에 Java 힙에서 java.lang.Class 객체를 생성하여 힙의 데이터와 연결합니다.
로드가 완료되면 이 바이트 스트림을 검증하기 시작합니다. (실제로 많은 단계는 위의 단계와 교차적으로 진행됩니다. 예를 들어, 파일 형식 검증):
검증의 목적: class 파일의 바이트 스트림 정보가 JVM의 취향에 맞도록 확실하게 하여, JVM이 불편하지 않도록 합니다. class 파일이 순수한 Java 코드로 컴파일된 경우, 배열 범위 초과, 존재하지 않는 코드 블록으로 이동과 같은 불건강한 문제가 발생하지 않습니다. 왜냐하면 이러한 현상이 발생하면 컴파일러가 컴파일을 거부합니다. 그러나, 이전에 말했듯이, Class 파일 스트림은 반드시 Java 소스 코드로부터 컴파일된 것은 아닌 경우도 있으며, 네트워크나 다른 곳에서 온 경우도 있으며, 나 자신이16진수를 쓰면, JVM이 이 데이터를 검증하지 않는다면, 해로운 바이트 스트림이 JVM을 완전히 붕괴시킬 수 있습니다.
검증은 주로 몇 가지 단계를 거쳐 이루어집니다: 파일 형식 검증->메타데이터 검증->바이트 코드 검증->기호 참조 검증
파일 형식 검증은 바이트 스트림이 Class 파일 형식 규범에 맞는지 검증하고, 현재 JVM 버전이 처리할 수 있는지 검증합니다. 문제가 없으면, 바이트 스트림은 메모리의 메서드 영역에 저장될 수 있습니다. 이후의3이 모든 검증은 메서드 영역에서 수행됩니다.
메타데이터 검증은 바이트 코드에 의해 설명된 정보를 의미화 분석하여, 설명된 내용이 java 언어의 문법 규범에 맞는지 확인합니다.
바이트 코드 검증은 가장 복잡하며, 메서드 본체의 내용을 검증하여 실행 시에 문제가 발생하지 않도록 보장합니다.
기호 참조 검증을 사용하여 참조의 진정성과 가능성을 검증합니다. 예를 들어, 코드에서 다른 클래스를 참조하는 경우, 이를 검증해야 합니다; 또는 코드에서 다른 클래스의 특성에 접근하는 경우, 이 특성의 접근 가능성을 검증합니다. (이 단계는 후속의 해석 작업을 위한 기초를 마련합니다.)
검증 단계는 중요하지만 필수적이지 않습니다. 어떤 코드가 반복적으로 사용되고 신뢰성이 검증된 경우, 실행 단계에서는-Xverify:none 매개변수를 사용하여 대부분의 클래스 검증 조치를 끄고, 클래스 로드 시간을 단축합니다.
위 단계가 완료되면, 준비 단계로 이동합니다:
이 단계에서는 클래스 변수(고정 변수를 의미합니다)에 대한 메모리를 할당하고, 그 값이 초기화된 단계로, 이 메모리는 메서드 영역에서 할당됩니다. 이 단계에서는 고정 변수에 대한 초기 값을 설정하는 것이고, 인스턴스 변수는 객체가 인스턴스화될 때 할당됩니다. 클래스 변수에 대한 초기 값 설정과 클래스 변수의 할당은 다릅니다. 예를 들어:
public static int value=123;
이 단계에서 value의 값은 0이 아니라123그런 이유로, 이 시점에서는 아직 어떤 java 코드도 실행되지 않았습니다.123아직 보이지 않습니다.而我们看到的把123value에 할당하는 putstatic 명령어는 프로그램이 컴파일된 후 <clinit>()에 존재하므로, value에 값을 할당하면123초기화 시에만 실행됩니다.
이것도 예외가 있습니다:
public static final int value=123;
여기서 준비 단계에서 value의 값은 초기화됩니다.123이것은, 컴파일 시점에서 javac가 이 특수한 value에 대해 ConstantValue 속성을 생성하고, 준비 단계에서 jm이 이 ConstantValue의 값을 기준으로 value에 값을 할당한다는 것입니다.
이전 단계를 완료한 후, 해석을 시작해야 합니다. 해석은 대체로 클래스의 필드, 메서드 등을 변환하는 것으로, Class 파일의 구조 내용에 대해 깊이 이해하지 않았습니다.
초기화 과정은 클래스 로드 과정의 마지막 단계입니다:
이전 클래스 로드 과정에서는 로드 단계에서 사용자가 커스터마이zed 클래스 로더를 통해 참여할 수 있지만, 다른 작업은 JVM이 주도합니다. 초기화 단계에 이르면 실제로 java 내의 코드를 실행하게 됩니다.
이 단계에서는 일부 전처리 작업을 수행합니다. 준비 단계에서 이미 클래스 변수에 대해 체계적으로 할당이 완료되었음을 구분합니다.
물론이죠, 이 단계는 프로그램의 <clinit>() 메서드를 실행하는 단계입니다. 아래에서 <clinit>() 메서드를 연구해 보겠습니다:
<clinit>() 메서드는 클래스 생성자 메서드로, 컴파일러가 클래스 내의 모든 클래스 변수 할당 작업과 정적 문장 블록 내의 문장을 결합하여 생성하며, 그 순서는 소스 파일에서의 순서와 같습니다.
<clinit>() 메서드는 클래스 생성자 메서드와 다르며, 부모 클래스의 <clinit>() 메서드를 명시적으로 호출할 필요가 없습니다. 가상기계는 서브 클래스의 <clinit>() 메서드가 실행되기 전에 부모 클래스의 이 메서드가 이미 실행되었음을 보장합니다. 즉, 가상기계에서 처음 실행되는 <clinit>() 메서드는 java.lang.Object 클래스입니다.
다음에 예제를 통해 설명해 보겠습니다:
static class Parent{ public static int A=1; static{ A=2; } } static class Sub extends Parent{ public static int B=A; } public static void main(String[] args){ System.out.println(Sub.B); }
먼저 Sub.B에서 정적 데이터를 참조하였기 때문에, Sub 클래스는 초기화되어야 합니다. 동시에, 부모 클래스 Parent는 먼저 초기화 작업을 수행해야 합니다. Parent 초기화 후, A=2;따라서 B=2;이 과정은 다음과 같습니다:
static class Parent{ <clinit>(){ public static int A=1; static{ A=2; } } } static class Sub extends Parent{ <clinit>(){ //jvm은 부모 클래스의 이 메서드를 먼저 실행한 후 여기서 실행합니다 public static int B=A; } } public static void main(String[] args){ System.out.println(Sub.B); }
<clinit>(); 메서드는 클래스와 인터페이스에 대해 필수적이지 않습니다. 클래스나 인터페이스에 대해 클래스 변수에 대해 할당이 없고 정적 코드 블록이 없으면, <clinit>() 메서드는 컴파일러에 의해 생성되지 않습니다.
인터페이스 내에 static{}와 같은 정적 코드 블록이 존재할 수 없지만, 여전히 변수 초기화 시 변수 할당 작업이 있을 수 있기 때문에 인터페이스 내에도 <clinit>() 생성자가 생성됩니다. 하지만 클래스와 다르게, 서브 인터페이스의 <clinit>(); 메서드를 실행하기 전에 부모 인터페이스의 <clinit>(); 메서드를 실행할 필요가 없습니다. 부모 인터페이스에서 정의된 변수가 사용될 때 부모 인터페이스가 초기화됩니다.
또한, 인터페이스의 구현 클래스는 초기화할 때 인터페이스의 <clinit>() 메서드를 실행하지 않습니다.
또한, JVM은 다중 스레드 환경에서 <clinit>(); 메서드가 올바르게 잠금 동기화될 수 있도록 보장합니다. <초기화는 한 번만 실행됩니다>.
다음에 예제를 통해 설명해 보겠습니다:
public class DeadLoopClass { static{ if(true){ System.out.println("["+Thread.currentThread()+"] 초기화가 완료되었습니다. 아래는 무한 루프를 시작합니다"); while(treu){} } } /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub System.out.println("toplaile"); Runnable run=new Runnable(){ @Override public void run() { // TODO Auto-generated method stub System.out.println("["+Thread.currentThread()+"] 해당 클래스를 인스턴스화 할 것입니다"); DeadLoopClass d=new DeadLoopClass(); System.out.println("["+Thread.currentThread()+"] 해당 클래스의 초기화 작업이 완료되었습니다"); }}; new Thread(run).start(); new Thread(run).start(); } }
이곳에서 실행할 때 블록이 발생할 것을 볼 수 있습니다.
읽어주셔서 감사합니다. 많은 도움이 되길 바랍니다. 많은 분들의 사이트 지원에 감사합니다!