[JVM] JVM의 init 메서드, 객체의 초기화를 위한 인스턴스 초기화 메서드(instance initialization method)
1. JVM의 init 메서드, 객체의 초기화를 위한 인스턴스 초기화 메서드
(instance initialization method)
[ 인스턴스 초기화 메서드 (instance initialization method) ]
JVM은 객체 인스턴스를 초기화 할 때 init이라는 고유한 메서드를 활용한다. 예를 들어 다음과 같이 Object 객체를 생성하는 코드가 있다고 하자.
Object obj = new Object();
해당 코드를 컴파일하고 javap -c 명령어로 바이트 코드를 살펴보면 다음과 같음을 확인할 수 있다.
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
이를 통해 JVM은 객체의 초기화를 위해 init이라는 특별한 이름의 메서드를 호출함을 알 수 있다. JVM 전문 용어로 이를 인스턴스 초기화 메서드(instance initialization method)라고 하는데, 아래의 조건을 만족하는 메서드만이 인스턴스 초기화 메서드에 해당한다.
- 클래스에 정의되어 있음
- 이름이 init임
- 반환 타입이 void임
사실 init 메서드는 생성자에 해당하기도 한다. 자바의 생성자는 자바 컴파일러에 의해 특별한 형태의 메서드인 init 메서드로 변환된다. 그리고 init 메서드가 JVM이 객체를 초기화할 때 호출되는 것이다. JVM은 이러한 방식을 통해 표준되고 일관된 객체 초기화 과정을 가질 수 있게 되었다.
하지만 자바는 인스턴스 초기화 블록(Instance Init Block) 역시 지원한다. 예를 들어 다음과 같은 Member 클래스가 있다고 할 때, 다음과 같이 인스턴스 초기화 블록을 활용할 수 있다.
public class Member {
private String name;
{
this.printName(name);
}
public Member(String name) {
System.out.println("Member Constructor called");
this.name = name;
}
public void printName(String name) {
System.out.println("name = " + name);
}
}
위와 같이 작성된 Member 객체를 생성하면 다음과 같이 출력문이 찍힘을 확인할 수 있다.
name = null
Member Constructor called
코드를 보면 생성자 이전에 인스턴스 초기화 블록이 실행되는 것을 확인할 수 있다. 위와 같이 출력이 된 이유는 해당 코드의 컴파일된 바이트 코드를 보면 된다. 다음은 컴파일된 클래스 파일을 디컴파일한 코드이다.
public class Member {
private String name;
public Member(String name) {
this.printName(this.name);
System.out.println("Member Constructor called");
this.name = name;
}
public void printName(String name) {
System.out.println("name = " + name);
}
}
이를 보면 인스턴스 초기화 블록이 생성자(init 메서드)로 들어가게 되고, 이로 인해 아직 name 필드에 값이 할당되지 않은 상태라 null이 출력됨을 확인할 수 있다. 즉, 생성자와 초기화 블록은 자바 코드 작성 시에는 분리되어 있지만, 컴파일된 바이트 코드 수준에서는 동일한 인스턴스 초기화 메서드(init)에 해당함을 확인할 수 있는 것이다.
그렇다면 다음과 같이 name 필드에 변수가 선언된 클래스라면 어떨까?
public class Member {
private String name = "MangKyu";
{
this.printName(name);
}
public Member(String name) {
System.out.println("Member Constructor called");
this.name = name;
}
public void printName(String name) {
System.out.println("name = " + name);
}
}
코드를 실행해보면 다음과 같이 출력됨을 확인할 수 있다.
name = MangKyu
Member Constructor called
이전과 다르게 name에 값이 할당된 상태임을 확인할 수 있는데, 해당 클래스의 바이트 코드를 보면 다음과 같다.
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #7 // String MangKyu
7: putfield #9 // Field name:Ljava/lang/String;
10: aload_0
11: aload_0
12: getfield #9 // Field name:Ljava/lang/String;
15: invokevirtual #15 // Method printName:(Ljava/lang/String;)V
18: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream;
21: ldc #25 // String Member Constructor called
23: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
26: aload_0
27: aload_1
28: putfield #9 // Field name:Ljava/lang/String;
31: return
가장 먼저 init 메서드를 통해 객체가 생성된 후에 곧바로 name 필드에 “MangKyu” 변수가 할당됨을 확인할 수 있다. 그리고 이후에 인스턴스 초기화 블록에 의해 printName가 호출되고, 곧이어 생성자가 호출됨을 확인할 수 있다. 따라서 객체의 생성 이후 값이 바로 할당되기 때문에 null이 찍히지 않는 것이다. 위의 컴파일된 코드를 디컴파일된 형태로 보면 다음과 같다.
public class Member {
private String name = "MangKyu";
public Member(String name) {
this.printName(this.name);
System.out.println("Member Constructor called");
this.name = name;
}
public void printName(String name) {
System.out.println("name = " + name);
}
}
참고로 클래스의 초기화를 위한 메서드 역시 존재하는데, 이는 static 블록을 지정해주면 되며, 컴파일 시에 cinit 메서드로 컴파일된다.
public class Member {
private String name = "MangKyu";
static {
System.out.println("Class Init called");
}
public Member(String name) {
System.out.println("Member Constructor called");
this.name = name;
}
public void printName(String name) {
System.out.println("name = " + name);
}
}
참고 자료