
Java의 실행 과정을 크게보면 총4단계로 구분 가능하다.
class Java{
public void test(){
~~
}
}
JDK안에
javac.exe(컴파일러 어플리케이션)를 실행시켜 Java.java 파일을 컴파일 합니다.
cmd 창을 켜 javap -v Java.java를 실행시키면 class 파일이 만들어 지는데
class 파일안에 보면 Constant pool이 존재하고 해당 메서드의 명령어 들이 바이트 코드로 변한되어 보여진다.
public class Example {
public void printMessage() {
System.out.println("Hello, World!");
}
}
바이트코드:
public void printMessage();
Code:
0: getstatic #7 // Field java/lang/System.out
3: ldc #13 // String "Hello, World!"
5: invokevirtual #19 // Method java/io/PrintStream.println
8: return
Constant Pool 내용:
#7 = Fieldref #2.#3 // java/lang/System.out
#13 = String "Hello, World!"
#19 = Methodref #4.#5 // java/io/PrintStream.println
Constant Pool의 구성 요소
상수(Constant)
실제로 "변하지 않는 값"으로 정의되는 데이터.
예: 문자열 리터럴, 숫자 상수, 기본 자료형 값.
String greeting = "Hello";
int number = 42;
"Hello": 문자열 상수.
42: 정수 상수.
심볼릭 참조(Symbolic Reference)
클래스, 메서드, 필드에 대한 메타데이터를 가리키는 참조 정보.
실행 시 JVM이 이를 동적으로 링크하여 실제 참조로 변환합니다.
예: 메서드 이름, 클래스 이름, 필드 이름 등.
예시:
System.out.println("Hello");
System.out: Fieldref로 저장.
println: Methodref로 저장.
자바 가상 머신(Java Virtual Machine)을 통해 Java.class 파일을 실행시킵니다.
JVM은 바이트코드를 해당 플랫폼에서 이해할 수 있는 형태로 해석하여 그 프로그램을 실행시켜 줍니다.

이 과정은 바이트 코드로 변환 된 .class가 Class Loader에 의해 JVM에 실행 시키는 과정이다.
큰 그림으로 보자면 Class Loader에 올릴 때 3단계의 절차가 존재한다.
로딩 -> 링크 -> 초기화가 끝나면 Methoad Area에 바이트 코드가 적재 된다.
Java 클래스는 Java.lang.ClassLoader인스턴스에 의해 로드됩니다. 그런데 ClassLoader도 클래스이기 때문에 ClassLoader.class는 누가 로드 해주는지가 문제입니다. 이런 ClassLoader.class 를 로드하는 것이 Bootstrap 클래스 로더(원시 클래스 로더) 입니다.
주로 $JAVA_HOME/jre/lib 디렉토리 에 있는 rt.jar 및 기타 핵심 라이브러리와 같은 JDK 내부 클래스를 로드하는 역할을 합니다.
모든 클래스 로더의 부모이기도 합니다. 그렇기 때문에 Bootstrap 클래스 로더는 자바가 아닌 네이티브 언어로 작성되어 클래스로더가 null로 표시됩니다.
확장 클래스로더는 부트스트랩 클래스로더를 부모로 갖는 클래스로더로써, 확장 자바 클래스들을 로드한다.
이 단계에서 우리가 코딩한 Class들이 로딩된다.
JVM명세가 정하는 규칙과 제약을 만족하는지 확인, 보안위협에 대한 검증 포함
ex)파일 형식, 바이트 코드 변환, 자바11로 컴파일 했으면 jvm도 11버전에 맞춰야한다. jvm이 운영체제 위에 돌아가는 하나의 프로세스다 이 바이트 코드가 OS에 손상을 줄 수있는 코드, API 검증
객체 인스턴스가 저장 될 메모리 공간을 확보하고 0, null, false로 초기화
public class Example {
static int count;
static String name;
static Object obj;
static final double PI = 3.14; // 상수
}
정적 필드에 기본값 할당:
클래스의 정적 변수(static fields)에 대해 메모리를 할당하고 기본값을 설정합니다.
기본값은 다음과 같이 설정됩니다:
숫자형(int, long 등): 0 또는 0L
boolean: false
참조형(Object, 배열 등): null
상수 풀(Constant Pool) 준비:
클래스 파일 내에 있는 상수 풀(문자열, 심볼 참조 등)을 JVM 내부 데이터 구조로 변환합니다.
이 시점에서는 아직 심볼릭 참조가 실제 메모리 주소로 변환되지 않습니다.
객체 인스턴스가 저장 될 메모리 공간을 확보하고 초기화값으로 초기화
상수 풀의 심벌 참조를 직접 참조를 대체하는 과정
public class ResolutionExample {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.sayHello(); // 심볼릭 레퍼런스를 사용해 메서드 호출
}
}
class MyClass {
public void sayHello() {
System.out.println("Hello, world!");
}
}
컴파일 시점:
ResolutionExample 클래스와 MyClass 클래스가 각각 컴파일됩니다.
MyClass 클래스의 sayHello 메서드와 관련된 심볼릭 레퍼런스는 아직 주소를 모른 채 이름만 존재합니다. 즉, 심볼릭 레퍼런스로 sayHello 메서드를 호출하려는 코드가 만들어집니다.
로드 시점 (클래스 로딩):
JVM은 ResolutionExample을 로드하고 실행 준비를 합니다.
MyClass 클래스가 필요하므로, MyClass 클래스도 로드합니다. 이때, 클래스 파일을 메서드 영역에 로딩합니다.
Resolution 단계:
JVM은 ResolutionExample 클래스 내에서 myClass.sayHello()를 호출할 때, 심볼릭 레퍼런스인 sayHello 메서드 호출을 실제 메모리 주소로 변환합니다.
이 변환은 메서드 영역에 저장된 sayHello 메서드의 실제 주소로 이루어집니다. 즉, 심볼릭 레퍼런스인 sayHello가 실제 메서드가 위치한 주소로 해결됩니다.
실제 호출:
sayHello 메서드의 주소가 결정되면, JVM은 이 메서드를 호출하고 실제로 "Hello, world!"를 출력합니다.
모든 정적 변수들을 정의된 값으로 초기화 시킵니다
Interpreter는 소스코드의 각 행을 연속적으로 해석하고 실행하고, JIT는 바이트코드 전체를 해석하는데, 전체를 프로그램 수행 초기에 컴파일 하게되면 속도가 너무 느리기 때문에 모든 코드 초기엔 인터프리터로 시작하고, 해당 코드의 중복이 많은 경우 JIT의해 컴파일을 수행하게 된다.
*JIT 컴파일러는 JVM의 핵심으로 JVM 성능에 카장 큰 영향을 준다.
class Example{
public void print();
}
3.1 객체의 클래스 메타데이터 확인
객체의 Object Header에는 해당 객체가 어느 클래스의 인스턴스인지 가리키는 포인터(클래스 참조)가 포함되어 있습니다.
이 포인터를 통해 JVM은 Method Area에서 Example 클래스의 메타데이터에 접근합니다.
3.2 vtable 검색
Example 클래스 메타데이터에는 vtable(메서드 테이블)이 포함되어 있습니다.
JVM은 print() 메서드가 vtable에서 몇 번째 슬롯에 있는지 확인하고, 해당 슬롯에서 메서드의 실제 네이티브 주소를 찾습니다.
3.3 메서드 실행
메서드의 네이티브 주소를 기반으로 실행 코드로 점프합니다.
만약 해당 메서드가 아직 JIT 컴파일되지 않았다면, 인터프리터가 바이트코드를 해석하며 실행합니다. 이후 JIT 컴파일러가 메서드를 컴파일하여 네이티브 코드로 변환하면, 다음 호출부터는 컴파일된 코드를 사용합니다.
맨처음 클래스들을 올리기 위해선 ClassLoader.class가 필요하다 그런데 JVM엔 아직 클래스로더가 없기 때문에 JVM 내부 네이티브 메서드를 호출하여 클래스 파일을 메모리에 로드하거나 정의합니다.