앞선, 1. 클래스 파일(.class)의 메모리 로드 과정 내용에 이어서 인스턴스(Instance) 생성과 생성자(Constructor) 호출 및 실행 과정을 다룰것이다. 생성자에 대한 내용은 Constructor에 설명되어있다.
[Java] 1. 클래스 파일의 메모리 로드 과정 - Bytecode 실행과 Runtime Data Area의 변화
OverviewJava 프로그램이 실행되면서 JVM의 메모리 구조는 어떻게 변화할까 궁금하였다. 해당 과정을 이해하기 위해, 바이트 코드를 명령어와 JVM의 Runtime Data Area와 맵핑시키면서 분석하였다. 또한
devjohnpark.tistory.com
Instance 생성
소스코드 중, AddExample addExample = new AddExample();에서 AddExample클래스의 클래스 파일이 dynamic loading과 linking 까지되었다.
이어서 new 명령어에 의하여, package_name/AddExample 클래스의 새로운(new) 인스턴스(instance)를 생성하여 힙(heap) 영역에 저장된다. 그리고 인스턴스 참조 값(인스턴스의 주소)는 Operand Stack에 push 된다. 스택 기반으로 연산을 수행하기 위해서, 연산의 중간값(operand)을 임시 저장하기 위한 용도로 피연산자 스택(Operand Stack)을 사용한다. 이로써 new 명령어의 실행이 마쳤다.
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #7 // class package_name/AddExample
3: dup
4: invokespecial #9 // Method package_name/AddExample."<
...
SourceFile: "Main.java"
이후에 인스턴스 변수를 초기화 하기 위해서, 생성자가 호출된다. 이를 수행하기 위해, 그전에 dup 명령어가 실행되어 Operand Stack의 top에 저장된 값인 인스턴스 참조 값(Instance reference value)을 duplicate하여서 push 된다.
Constructor 호출과 실행
생성된 인스턴스의 생성자 호출
인스턴스 생성 이후, 위의 바이트 코드에서 생성자를 호출하는 invokespecial #9 명령어는 Operand Stack에 있는 복사된 인스턴스 참조 값(Instance reference value)을 pop을 한다. 그리고 AddExample 인스턴스의 런타임 상수 풀의 인덱스 #9를 따라 참조하면 package_name/AddExample 객체의 기본 생성자(default constructor)인 "<init>":()V를 호출한다.
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // package_name/AddExample
#8 = Utf8 package_name/AddExample
#9 = Methodref #7.#3 // package_name/AddExample."<init>":()V
...
SourceFile: "Main.java"
package_name/AddExample."<init>":()V은 아래의 AddExample 클래스의 기본 생성자(AddExample())를 호출하도록 지시하는 심볼릭 참조이다. 소스코드에서 생성자가 정의되어있지 않아, 컴파일러가 자동으로 기본 default constructor를 추가하고 컴파일되었다.
public class AddExample {
public AddExample() { }
}
생성된 인스턴스의 생성자 실행
생성자가 호출되면, 아래의 package_name/AddExample 클래스의 생성자 메서드(package_name.AddExample())의 바이트 코드 명령어가 수행된다.
public package_name.AddExample();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
SourceFile: "AddExample.java"
aload_0 명령어는 Local Variable Array의 0번 slot의 값을 Operand Stack에 로드하라는 명령어이다. 인스턴스 메서드에서의 this 참조 변수에 저장된 값(Value of this)은 Local Variable Array의 0번 slot에 자동으로 저장된다. 아 값을 Operand Stack에 로드하는 것이다.
여러 지역 변수(local variable)에 저장된 값을 지역 변수 배열(Local Variable Array)의 각 슬롯(slot)에 저장된다. 이후 지역 변수 배열로부터 연산에 필요한 값을 피연산자 스택(Operand Stack)에 push하는 동작 방식이다.
인스턴스 메서드의 작업을 수행할때, 인스턴스에 참조하여 인스턴스 변수 값을 읽어 오거나 수정을 해야한다. 그러므로 이렇게 기본적인 인스턴스 메서드의 동작을 보장하기 위해서, 인스턴스 참조값이 필요하다. 따라서 인스턴스가 생성되고 생성자가 호출되기전에, 인스턴스 자신을 가리키는 참조 변수인 this에 저장된 값을 Local Variable Array의 0번 slot에 디폴트로 저장한다.
예시로 아래의 Example 생성자를 보면 동일한 매개변수명과 인스턴스 변수명을 구별하기 위해 this를 사용하였고, this 참조 변수로 인스턴스에 참조하여 인스턴스 변수 값을 초기화하였다. 이렇게 this 참조는 현재 객체(인스턴스)를 가리키며, 인스턴스 변수명과 동일한 매개변수명으로 값을 전달받기 위해 this 참조 변수가 필요하다.
public class Example {
int value;
Example(int value) {
this.value = value;
}
}
이처럼 생성자 내에서 인스턴스 변수를 초기화하거나 메서드를 호출할 때 this 참조가 필요하므로 aload_0 명령어의 실행이 필요한 것이다.
인스턴스 참조 변수인 this는 메서드 내의 지역변수이다. 지역변수는 this처럼 주소 값을 저장하거나 정수값을 저장하는 등 타입이 다르다. 이러한 지역 변수에 저장된 타입이 다른 값들을 지역 변수 배열에 어떻게 저장할까?
지역 변수 배열은 일반적인 배열과 다르게, 슬롯의 값을 4byte 단위로 저장하여서 여러 타입도 저장이 가능하다. 아래왜 같이, 슬롯에 저장되는 값은 데이터 값 자체거나, 데이터가 저장된 주소이다.
0번 슬롯 1번 슬롯 2번 슬롯 3번 슬롯
+-----------------+-----------------+-----------------+-----------------+
| 4byte (int) | 4byte (long 하위)| 4byte (long 상위)| 4byte (참조 타입) |
+-----------------+-----------------+-----------------+-----------------+
같은 메모리 용량으로 값을 저장하여서 기준 주소에서 offset 만큼 떨어진 값을 찾으면된다. 기준 주소는 지역 변수 배열을 가리키는 참조 변수에 저장된 값이며, offset은 4byte x index 값이다.
이처럼 지역 변수에 저장된 값을 지역 변수 배열에 저장하면, 빠르게 각 슬롯의 값에 접근이 가능하다. 이러한 매커니즘을 적용하기 위해서, 컴파일 타임에 java 컴파일러가 지역 변수에 저장되는 값을 지역 변수 배열에 저장하는 바이트 코드로 컴파일하는 것이다.
부모 클래스의 생성자 호출
해당 클래스(AddExample)의 생성자가 실행되고나서, 다음으로 부모 클래스의 생성자(Object())를 호출한다. invokespecial #1 명령어로 부모 클래스의 생성자가 호출된다. 자식 클래스의 생성자에서 부모 클래스의 생성자를 호출하는 super()소스 코드에 대한 바이트코드이다.
부모 클래스에 기본 생성자만 있을 때, 자식 클래스의 생성자에서 super()를 생략하면, 자바 컴파일러는 자동으로 부모 클래스의 기본 생성자(super())를 호출하는 바이트 코드를 삽입한다. 그리하여 클래스 파일에 부모 클래스 기본 생성자 호출하는 바이트 코드가 저장된것이다.
자식 클래스의 인스턴스는 부모 클래스의 필드와 메서드를 상속받으므로, 자식 클래스의 인스턴스는 부모 클래스의 생성자를 호출할 수 있다.
Object 클래스는 모든 자바 클래스의 최상위 부모 클래스이므로, AddExample클래스의 인스턴스가 부모 클래스(Object)의 생성자를 기본적으로 호출한다.
이로써, 자식 클래스의 인스턴스 생성 직후에 부모 클래스의 상태(변수에 저장된 값)를 동기화하기 위해, 부모 클래스의 생성자를 호출하여 부모 클래스의 인스턴스 변수를 초기화한다. 마저 본인 클래스의 생성자에 대한 수행을 마치면, 해당 인스턴스의 초기화 작업까지 수행된 것이다.
참고자료
- https://docs.oracle.com/javase/specs/jvms/se21/html/index.html
- Inside the Java Virtual Machine (Bill Venners)
'Java > JVM' 카테고리의 다른 글
[JVM] Garbage Collection 개념과 동작원리 (0) | 2025.07.17 |
---|---|
[Java] 인스턴스 메서드 호출 및 실행 과정 - Bytecode 실행과 Runtime Data Area의 변화 (3) (0) | 2024.06.16 |
[Java] 클래스 파일의 메모리 로드 과정 - Bytecode 실행과 Runtime Data Area의 변화 (1) (0) | 2024.06.14 |
[Java] JVM과 OS의 호환성 (0) | 2024.05.31 |
[Java] JVM과 CPU의 호환성 (1) | 2024.05.31 |