클래스 파일 - JVM 밑바닥까지 파헤치기

이상윤·2026년 2월 3일

클래스 파일의 구조

가장 먼저, 클래스 파일의 구조부터 살펴보자.

자바 가상 머신 명세에 따르면, 클래스 파일에 데이터를 저장하는데는 C언어의 구조체와 비슷한 의사 구조를 이용한다. 이 의사 구조에는 부호 없는 숫자와 테이블이라는 두 가지 데이터 타입만 존재한다.

  • 부호 없는 숫자(unsign number): 기본 데이터 타입을 포함하며, u1, u2, u4, u8은 각각 1바이트, 2바이트, 4바이트, 8바이트를 표현한다. 숫자, 인덱스 참조, 수랑값을 기술하거나 UTF-8로 인코딩된 문자열 값을 구성할 수 있다.
  • 테이블: 여러 개의 부호 없는 숫자나 또 다른 테이블로 구성된 복합 데이터 타입을 표현한다. 구분이 쉽도록 테이블 이름은 관례적으로 _info로 끝나며, 테이블은 계층적으로 구성된 복합 구조의 데이터를 설명하는데 사용된다. 클래스 파일 전체는 본질적으로 테이블이며 구조는 다음과 같다.


여기서 같은 타입의 데이터 여러개를 표현할 때 그 개수가 정해져 있지 않다면 *_count 형태로 개수를 알려주며, 이처럼 {개수 + 개수만큼의 데이터 타입} 형태를 해당 타입의 컬렉션이라고 한다.

이제 위 클래스 파일 구조의 항목들에 대해 알아보자.

매직 넘버와 클래스 파일의 버전

모든 클래스 파일의 처음 4바이트는 매직 넘버로 이루어져 있으며, 이는 가상 머신이 허용하는 클래스 파일인지 여부를 확인하는데 사용된다.
매직 넘버 다음의 4바이트는 클래스 파일의 버전 번호이고, 5~6번째 바이트는 마이너 버전, 7~8번째 바이트는 메이저 버전을 의미한다. 자바 버전 번호는 45부터 시작한다. JDK 1.1이후로 주요 JDK의 릴리스 버전은 1씩 증가하며, 상위 버전 JDK는 하위 버전을 인식할 수 있지만 하위버전 JDK는 상위 버전의 클래스 파일을 실행할 수 없다.

참고로 JDK1.2 부터는 마이너 버전을 사용하지 않아 모두 0으로 고정되어 있으며, JDK 12부터 일부 복잡한 새 기능을 공개 베타 형태로 출시할 때 65535로 지정해 자바 가상 머신이 인지할 수 있도록 하였다.

상수 풀

상수 풀은 클래스 파일의 자원 창고라 할 수 있다. 클래스 파일 구조에서 다른 클래스와 가장 많이 연관된 부분이기도 하고, 차지하는 공간도 대체로 가장 크다.

상수 풀에 들어있는 상수의 수는 고정적이지 않으므로 이를 알려주는 u2타입 데이터가 필요하다. 이 개수를 셀때는 0이 아닌 1부터 시작함에 유의하자. 0번째를 비운 이유는 상수 풀 인덱스를 가르키는 데이터에서 '상수 풀 항목을 참조하지 않음'을 표현해야 하는 특수한 경우에 인덱스를 0으로 설정하기 위함이다.

클래스 파일 구조에서 오직 상수 풀만이 개수를 1부터 세고 나머지 카운트는 전부 0부터 센다.

상수 풀에는 두 가지 유형의 상수가 담기며, 리터럴과 심벌 참조이다.
리터럴은 자바 언어 수준에서 이야기하는 상수와 비슷한 개념이다.
심벌 참조는 컴파일과 관련된 개념이며, 다음 유형의 상수들이 포함된다.

  • 모듈에서 export하거나 import하는 패키지
  • 클래스와 인터페이스의 완전한 이름
  • 필드 이름과 서술자
  • 메서드 이름과 서술자
  • 메서드 핸들과 메서드 타입
  • 동적으로 계산되는 호출 사이트와 동적으로 계산되는 상수

상수 풀 안의 상수 각각은 모두 테이블이며, JDK21을 기준으로 총 17가지의 상수 타입이 존재한다. 이 17가지 타입의 테이블들은 공통적으로 u1타입의 플래그 비트로 시작하며, 그 값은 현재 상수가 속한 상수 타입을 나타낸다.

상수 풀의 각 항목 타입은 다음과 같다.

javap 명령어를 사용하면 상수 풀의 내용을 볼 수 있다.

아래 코드의 예시이다.

package org.example;

public class TestClass {
    private int m;

    public int inc(){
        return m + 1;
    }
}

.class 파일(bytecode)

javap -v .\target\classes\org\example\TestClass.class 결과

public class org.example.TestClass
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // org/example/TestClass
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
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 = Fieldref           #8.#9          // org/example/TestClass.m:I
   #8 = Class              #10            // org/example/TestClass
   #9 = NameAndType        #11:#12        // m:I
  #10 = Utf8               org/example/TestClass
  #11 = Utf8               m
  #12 = Utf8               I
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lorg/example/TestClass;
  #18 = Utf8               inc
  #19 = Utf8               ()I
  #20 = Utf8               SourceFile
  #21 = Utf8               TestClass.java
{
  public org.example.TestClass();
    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
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lorg/example/TestClass;

  public int inc();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lorg/example/TestClass;
}
SourceFile: "TestClass.java"

이렇게 상수 풀 내의 내용들을 확인할 수 있다. 하지만 여기서 ()I, ()V, LineNumberTable같은 소스 코드에서 찾아볼 수 없는 내용들이 있는데 이는 컴파일러가 자동으로 생성한 것들로 나중 포스트에서 알아보겠다.

또한, 17개 상수 유형의 구조는 다음과 같다.

접근 플래그

상수 풀 다음의 2바이트는 현재 클래스의 접근 정보를 식별하는 접근 플래그이다. 현재 클래스 파일이 표현하는 대상이 클래스인지 인터페이스인지, public인지, abstract인지, 클래스인 경우 final인지 등의 정보가 담긴다. 접근 플래그의 종류와 의미는 다음과 같다.


access_flags의 크기는 2바이트이므로 최대 16개의 플래그 비트를 사용할 수 있지만, 현재는 9개만 정의되어 있으며 정의되지 않은 플래그 비트의 값은 모두 0이어야 한다.

클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스

이 정보들은 클래스 파일의 상속 관계를 규정하며, 앞의 두 인덱스는 u2타입이고 세 번째 인덱스는 u2타입 데이터들의 묶음이다.

클래스 인덱스와 부모 클래스 인덱스

클래스 인덱스와 부모 클래스 인덱스는 각각 현재 클래스와 부모 클래스의 완전한 이름을 결정하는데 쓰인다. 여기서 자바 언어는 다중 상속을 허용하지 않으므로 부모 클래스 인덱스는 하나뿐이다. 또한 여기서 모든 클래스들의 부모인 java.lang.Object만 부모 클래스가 존재하지 않으므로, java.lang.Object를 제외한 모든 자바 클래스의 부모 인덱스 클래스 값은 0이 아니다.

인터페이스 인덱스

인터페이스 인덱스 컬렉션은 현재 클래스가 구현한 인터페이스들을 기술한다.
컬렉션 내의 인터페이스 순서는 자바 코드에서 implements키워드 뒤에 나열한 순서를 따른다.

클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스는 모두 접근 플래그 뒤에 나오며 클래스 인덱스와 부모 클래스 인덱스의 값은 CONSTANT_Class_info 타입의 클래스 서술자 상수를 가리킨다. 또한 클래스의 완전한 이름 문자열은 CONSTANT_Class_info 타입에 담긴 상수의 값을 인덱스로 하는 CONSTANT_Utf8_info 타입으로 정의된다.


인터페이스 인덱스 컬렉션의 첫 항목은 u2타입이며, 값은 인덱스 테이블의 크기. 즉, 현재 클래스가 구현한 인터페이스의 수이다.

필드 테이블

필드 테이블은 인터페이스나 클래스 내에 선언된 변수들을 설명하는데 쓰인다. 자바 언어에서 필드란 클래스 변수와 인스턴스 변수를 뜻하며, 메서드 내에 선언된 지역 변수는 필드가 아니다.

필드 테이블의 구조는 다음과 같다.

필드의 access_flags 항목이 가질 수 있는 값은 클래스의 access_flags와 매우 비슷하며, 데이터 타입은 u2이고 지원하는 항목은 다음과 같다.

access_flags다음에는 name_index와 descriptor_index가 온다. 이 둘은 상수 풀에서 인덱스로, 각각 필드의 단순 이름과 필드 및 메서드 서술자 참조를 가리킨다.

메서드 테이블

메서드 테이블의 구조는 필드 테이블의 구조와 완전히 흡사하며, 이는 다음과 같다.

각 데이터 항목의 의미도 일부를 제외하면 거의 비슷하다. 접근 플래그와 속성 테이블 컬렉션에서 선택할 수 있는 값만 살짝 다른 뿐이다. 메서드에서는 volatile과 transient키워드를 붙일 수 없으므로 메서드 테이블의 접근 플래그에는 ACC_SYNCHROINZED, ACC_NATIVE, ACC_STRICTFP, ACC_ABSTRACT 플래그가 추가되었다. 메서드 테이블에서 이용할 수 있는 플래그 종류와 값은 다음과 같다.

이렇게 메서드 정의를 명확하게 표현할 수 있다. 그러면 메서드 본문은 어디 있을까?
메서드 본문은 javac 컴파일러에 의해 바이트코드로 변환된 후 메서드 속성 테이블 컬렉션의 Code 영역에 따로 저장된다.

속성 테이블

그럼 이제 속성 테이블에 대해 알아보자.

속성 테이블은 다른 데이터 항목들에 비해 순서, 길이, 내용 등의 제약이 살짝 느슨하며 순서에도 엄격하지 않다. <자바 가상 머신 명세>에서도 기존 속성 이름과 중복되지 않는 한, 자체 제작한 컴파일러가 새로운 속성 정보를 속성 테이블에 추가할 수 있도록 하고 있다.

자바 가상 머신 명세가 인식하지 못하는 속성은 무시해버리며, JDK 21에서는 총 30개의 속성이 존재한다. 속성 목록은 다음과 같다.

속성 타입은 모드 CONSTANT_Utf8 타입 상수를 참조해 표현하며, 속성값의 길이는 u4타입으로 표현된다. 이때, 속성값 자체의 구조는 완벽하게 사용자 정의가 가능하며, 속성 테이블의 구조는 다음을 만족시켜야한다.

0개의 댓글