..

Java class 파일의 구조: constant pool부터 attribute까지

TOC


  1. Overview
  2. class 파일은 왜 필요한가
  3. class 파일의 주요 구성요소
  4. javap로 보는 class 파일
  5. Conclusion
  6. Next Step

Overview


앞 글에서는 Java 소스 코드에서 class 파일까지 흐름을 봤다.

이번 글에서는 class 파일 안에 무엇이 들어가는지만 간단히 본다.

class 파일은 왜 필요한가


Java는 소스 코드를 바로 실행하지 않는다. 먼저 class 파일을 만들고, JVM은 그 파일을 읽어서 실행한다.

즉, class 파일은 Java 실행의 중간 결과물이고, JVM이 프로그램을 이해하는 기본 단위다.

예를 들어 아래 코드를 보자.

public class Sample {
    private final String name;

    public Sample(String name) {
        this.name = name;
    }

    public String greet() {
        return "hello " + name;
    }
}

이 파일이 class로 바뀌면 JVM은 Sample이라는 이름, 생성자, greet() 메서드, name 필드를 따로 읽는다.

class 파일의 주요 구성요소


class 파일은 아래와같은 항목으로 구성되어있다.

  • magic number와 version: 파일 형식과 호환 버전
  • constant pool: 문자열, 클래스명, 메서드/필드 참조
  • 클래스 정보: 이름, 상속 관계, 접근 제한자
  • field / method: 어떤 멤버가 있는지와 그 구현
  • attribute: 부가 정보와 디버그 정보

특히 constant pool과 method가 중요하다. constant pool은 실행에 필요한 참조를 모아두고, method에는 실제 bytecode가 들어간다.

Sample 예제로 보면 이렇게 나뉜다.

// source
public class Sample {
    private final String name;

    public Sample(String name) {
        this.name = name;
    }

    public String greet() {
        return "hello " + name;
    }
}
// class file에서 읽히는 정보
class name: Sample
super class: java/lang/Object
field: name:Ljava/lang/String;
method: <init>(Ljava/lang/String;)V
method: greet()Ljava/lang/String;
constant pool: "hello ", Sample, name, java/lang/Object

사람이 읽는 문장과 JVM이 읽는 정보는 같은 모양이 아니다.

javap로 보는 class 파일


javap는 class 파일을 볼 때 가장 쉬운 도구다.

javac Sample.java
javap -c -v Sample

이 명령을 실행하면 크게 세 부분을 본다.

Class Metadata

Classfile .../Sample.class
  minor version: 0
  major version: 61
  this_class: #8 // Sample
  super_class: #2 // java/lang/Object
  interfaces: 0, fields: 1, methods: 3, attributes: 3

이 블록은 클래스 이름, 상속 관계, 필드/메서드 수처럼 파일의 뼈대를 보여준다.

Constant Pool

Constant pool:
  #7  = Fieldref      #8.#9   // Sample.name:Ljava/lang/String;
  #13 = InvokeDynamic #0:#14  // makeConcatWithConstants
  #17 = Fieldref      #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
  #23 = String        #24     // kim
  #25 = Methodref     #8.#26  // Sample."<init>":(Ljava/lang/String;)V
  #28 = Methodref     #8.#29  // Sample.greet:()Ljava/lang/String;
  #32 = Methodref     #33.#34 // java/io/PrintStream.println:(Ljava/lang/String;)V

이 블록은 문자열과 메서드/필드 참조 같은 실행용 참조를 보여준다.

Method Bytecode

public Sample(java.lang.String);
  0: aload_0
  1: invokespecial #1   // Object.<init>()
  4: aload_0
  5: aload_1
  6: putfield      #7   // name

public java.lang.String greet();
  0: aload_0
  1: getfield      #7   // name
  4: invokedynamic #13
  9: areturn

public static void main(java.lang.String[]);
  0: getstatic     #17  // System.out
  3: new           #8   // Sample
  7: ldc           #23  // "kim"
  9: invokespecial #25  // <init>
 12: invokevirtual #28  // greet
 15: invokevirtual #32  // println
 18: return

이 블록은 각 메서드가 실제로 어떤 bytecode로 풀리는지 보여준다.

보는 순서는 간단하다.

  1. header에서 클래스 버전과 상속 관계를 본다.
  2. Constant pool에서 문자열과 참조를 본다.
  3. 각 method의 Code에서 bytecode 흐름을 본다.

이렇게 보면 Sample 소스의 각 부분이 class 파일 안에서 어디로 갔는지 바로 연결된다.

Conclusion


class 파일은 소스 코드를 직접 담는 파일이 아니라, JVM이 읽을 메타데이터와 bytecode 묶음이다.

이 구조를 이해하면 javac가 무엇을 만들고, java가 무엇을 읽는지 훨씬 분명해진다.

Next Step


다음 글에서는 class 로딩, 링킹, 초기화를 본다.