..

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

TOC


  1. Overview
  2. class 파일은 왜 필요한가
  3. class 파일의 주요 구성요소
  4. javap로 보는 class 파일
  5. 정리
  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 numberversion: 파일 형식과 호환 버전
  • constant pool: 문자열, 클래스명, 메서드/필드 참조
  • 클래스 정보: 이름, 상속 관계, 접근 제한자
  • field / method: 어떤 멤버가 있는지와 그 구현
  • attribute: 부가 정보와 디버그 정보

특히 constant poolmethod가 중요하다. 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 파일


javapclass 파일을 볼 때 가장 쉬운 도구다.

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. methodCode에서 bytecode 흐름을 본다.

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

정리


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

정리하면 이런 질문에 답할 수 있다.

  • "class" 파일에는 왜 소스 코드 전체가 없지? -> JVM이 필요한 정보만 쓰기 때문이다.
  • "문자열은 어디에 저장되지?" -> constant pool과 메서드 bytecode에 나뉘어 들어간다.
  • "메서드는 어떻게 저장되지?" -> 메서드 시그니처와 실행용 bytecode가 별도로 들어간다.

Next Step


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