본문 바로가기

기술스택/ASM - [Bytecode]

[ASM] static과 인스턴스 필드 생성자 호출 - <clinit>, <init>

목차

    <clinit> : 정적 초기화 생성자 블록, <init> : 생성자 호출 블록

     

    변수를 만드는 것과, 생성자를 불러서 초기 값을 지정하는 것은 다른 프로세스이다.
    쉽게 말해서 Obj obj; 와 Obj obj = new Obj(); 은 JVM 동작 자체가 다르다. 

     

    asm으로 변수를 생성하는 것과는 별개로, clinit과 init 호출을 통해 이것의 생성자를 mv으로 호출하고, 이 생성자들을 통해 값을 넣어줄 수 있다

     

    clinit과 init은 Java 바이트코드에서 특별한 의미를 가지는 메서드 이름이다.

    이들은 클래스 단위로 작동하며, 특정 객체 인스턴스에 대한 것이 아니다. 이 메서드를 통해 정의된 코드는 클래스가 JVM에 의해 로딩되는 시점에 한 번만 실행된다.

     

    <clinit> : static에서의 생성자 호출

     

    <clinit>는 Java 클래스의 정적 초기화 블록(static initializer block)에 해당하는 특별한 메서드이다.

    Java에서 정적 초기화 블록은 클래스가 로딩되어 JVM(Java Virtual Machine)에 의해 처음으로 참조될 때 정확히 한 번 실행된다.

     

    이 블록은 주로 정적 변수의 복잡한 초기화에 사용되며, 모든 정적 변수 할당과 정적 초기화 블록 내의 명령어들은 <clinit> 메서드로 컴파일된다.

     

        public void example() {
    
            //2. 정적 변수(static)에 값 넣기
            //먼저 Java 클래스의 정적 초기화 블록을 호출한다.
            MethodVisitor mv = cv.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
            mv.visitCode();
            mv.visitLdcInsn(123); //stack에 상수 123 입력
            
            //이 static 변수에 123을 넣자.
            mv.visitFieldInsn(PUTSTATIC, "com/dummy/jdbcserver/example_asm/chap3/BasicExample" , "staticVal", "I");
            
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }

     

    <init> : 일반 인스턴스 필드의 생성자 호출

    반대로 <init>은 인스턴스 생성자를 나타낸다.

    이는 객체가 생성될 때마다 호출되며, 인스턴스 필드의 초기화나 생성자에서 정의된 다른 초기화 코드를 실행한다. 각각의 객체 인스턴스에 대해 <init> 메서드가 호출되며, 클래스 내에 여러 생성자가 정의될 수 있다. 각 생성자는 서로 다른 시그니처(매개변수 목록)를 가질 수 있다.

     

     

    다른 생성자 생성 예시

        public void example() {
            //1. 일반 필드 추가 및 값 넣기 (변수가 있다면 visitField 수행, 아니라면 새로 생성)
            FieldVisitor fv = cv.visitField(ACC_PUBLIC, "makeNewField", "I", null, null);
            if (fv != null) {
                //1-1. 커스텀 생성자 만들기
    
                //1-2. 기본 생성자 예시
    //            MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); //생성자 만들기
                //하지만 위처럼 실행하면 ClassFormatErr 발생, 이미 기본 생성자가 java에서는 알아서 생성되기 때문(똑같은 생성자 만들면 중복 에러)
    
                //1-3. 커스텀 생성자
                //그렇기에 매개변수를 다르게 하여, 다른 생성자를 추가하겠음. : 이제 Int형 변수를 받는다.
                MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "<init>", "(I)V", null, null); //생성자 만들기
                mv.visitCode(); //방문 시작
    
                //1-4. Object(상위)의 기본 생성자 호출 : java에서는 하위 클래스 생성자 호출 시 상위도 같이 호출하기 때문.
                // 이는 우리가 암묵적으로 사용하던 부모 클래스 생성자 호출 super()와 동일하다고 이해하면 된다.
                mv.visitVarInsn(ALOAD, 0); // this 참조
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
    
                //1-5. 커스텀 생성자를 통한 매개변수 값 할당
                mv.visitVarInsn(ALOAD, 0); //this(자기자신 - 커스텀 생성자) stack에 넣기
                mv.visitLdcInsn(1000); //stack에 값 넣기
                mv.visitFieldInsn(PUTFIELD, "com/dummy/jdbcserver/example_asm/chap3/BasicExample", "makeNewField", "I");
                mv.visitInsn(RETURN);
                mv.visitMaxs(0,0);
                mv.visitEnd();
                fv.visitEnd();
            }

     

     

    결과 : 

     

    public class BasicExample {
        public int val;
        public static int staticVal = 123;
        public int vFive;
        public int vSix;
        public int makeNewField;
    
        public BasicExample() {
            this.vFive = 5;
            this.vSix = 6;
        }
    
        public void main() throws Exception {
            System.out.println("기초 연습용");
        }
    
        //이 커스텀 생성자가 추가되었음
        public BasicExample(int var1) {
            this.makeNewField = 1000;
        }

     

    Class A 안에 a, b, c의 인스턴스가 있다면?

    public class A {
        int a, b, c; // 인스턴스 필드
    
        public A() { // 생성자
            a = 1;
            b = 2;
            c = 3;
        }
    }

     

    1. 클래스 A 안에 a, b, c 인스턴스 필드가 있다고 할 때, <clinit>를 통해 정의된 값은 이 필드들을 직접 초기화하지 않는다.

     

    2. 대신, 클래스 레벨의 정적 필드 초기화에 사용된다. 예를 들어, A 클래스의 정적 필드를 초기화하는 데 사용된다.

     

    3. 인스턴스 필드 a, b, c를 초기화하려면, <init> 메서드(즉, 생성자) 내에서 초기화 코드를 작성해야 한다. 각 객체 인스턴스에 대해 생성자 <init>가 호출되며, 이 때 객체의 인스턴스 필드를 초기화할 수 있다.

     

    ASM에서는 다음과 같이 나타낼 수 있다.

     

    MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
    mv.visitCode();
    // 객체 자신에 대한 참조와 필드 초기화 코드
    mv.visitVarInsn(ALOAD, 0); // 객체 자신(this)을 로드
    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); // 슈퍼클래스의 생성자 호출
    mv.visitVarInsn(ALOAD, 0);
    mv.visitLdcInsn(1);
    mv.visitFieldInsn(PUTFIELD, "A", "a", "I");
    // b, c 필드에 대한 초기화 코드 생략
    mv.visitInsn(RETURN);
    mv.visitMaxs(-1, -1); // COMPUTE_MAXS 옵션 사용 시 실제 값은 자동 계산됨
    mv.visitEnd();