7 minute read

클래스 파일 구조

플랫폼 독립을 향한 초석

Java는 가상 머신에 의해 운영 체제나 기계어에 종속되지 않는, 플랫폼 독립적 저장 형식을 택한다.

또한, Java는 초기 설계 단계부터 가상 머신에서 다른 언어를 실행할 가능성을 염두에 두고 제작되어 타 언어를 지원할 수 있도록 하였다. (언어 독립성)

이러한 특성 덕에, 오늘날 Scala, Kotlin, Groovy 등 다양한 언어가 JVM에서 실행될 수 있다.

이러한 언어 독립성을 보장하는 핵심은 가상 머신바이트코드 저장 형식이다.

  • 바이트코드 저장 형식 (.class)
    • 컴파일러가 자바 class(.java)들을 바이트코드 파일(.class)로 변환한다.
  • 가상 머신
    • 바이트코드 파일을 클래스 로더에 의해 해석하고, JVM 메모리에 로드시킨다.

클래스 파일 구조 및 속성

.class 파일 구조

Java의 클래스 파일 기본 구조는 같은 의사 구조(C의 구조체와 유사하다.)를 따른다. 이 구조는 부호 없는 숫자와 테이블로 구성된다.

  • 부호 없는 숫자
    • 기본 데이터 타입
    • u1, u2, u4, u8(1, 2, 4, 8바이트)
  • 테이블
    • 여러개의 부호 없는 숫자나 다른 테이블로 구성된 복합 데이터 타입
    • 관례적으로 _info로 끝난다.

각 항목은 아래와 같이 정해진 순서에 따라 나열된다.

클래스 메타 정보

  • 매직 넘버
    • 모든 클래스 파일의 처음 4바이트는 매직 넘버로 시작한다.
    • 해당 파일이 가상 머신이 허용하는 .class 파일임을 명시한다. (OXCAFEBABE)
  • Minor version(minor_version)
    • 2바이트로 JDK 마이너 버전을 명시한다.
  • Major version(major_version)
    • 2바이트로 JDK 메이저 버전을 명시한다.
  • Constant pool
    • constant_pool_count : 2바이트로, 상수 풀 항목 개수를 알려준다.
    • constant_pool\[]
      • 각 원소는 cp_info 구조를 따른다.
      • 문자열, 클래스와 인터페이스명, 필드명 등 현재 클래스 파일의 다양한 상수를 명시한다.
      • constant_pool 테이블의 시작은 tag 바이트로 시작한다.
      • 1부터  constant_pool_count- 1 까지의 인덱스를 가진다.
      • 0번째 인덱스: 상수 풀 인덱스를 가리키는 데이터에서 상수 풀 항목을 참조하지 않음을 표현해야 하는 특수한 경우에, 이 인덱스를 0으로 설정한다.
cp_info {
    u1 tag;
    u1 info[];
}
  • Access flags(access_flags)
    • 2바이트로, 해당 클래스 혹은 인터페이스 접근 권한을 명시한다.
Flag명 설명
ACC_PUBLIC 0x0001 public으로 선언. 패키지 외부에서 접근 가능.
ACC_FINAL 0x0010 final로 선언. 하위 클래스가 허용되지 않음.
ACC_SUPER 0x0020 _invokespecial_ 명령어에 의해 상위 클래스 메서드를 특별히 처리함.
ACC_INTERFACE 0x0200 인터페이스
ACC_ABSTRACT 0x0400 abstract로 선언. 인스턴스화 불가
ACC_SYNTHETIC 0x1000 synthetic으로 선언. 소스 코드에 존재하지 않음.
ACC_ANNOTATION 0x2000 애노테이션 타입으로 선언.
ACC_ENUM 0x4000 enum 타입으로 선언.
ACC_MODULE 0x8000 모듈인지 여부

다음 클래스 파일의 정보들은 클래스 파일 간 상속 관계를 정의하며, 인덱스 정보들은 상수 풀에 대해 유효한 인덱스로 구성된다.

  • This class(this_class)
    • 현재 클래스의 인덱스를 표기한다.
  • Super class(super_class)
    • 부모 클래스의 인덱스를 표기한다.
    • java.lang.Object를 제외한 모든 자바 클래스의 부모 클래스 인덱스는 값이 0이 아니다.
  • Interfaces
    • interfaces_count
      • 현재 클래스 / 인터페이스의 direct superinterface 개수를 표기한다.
    • interfaces[]
      • 구현된 인터페이스의 순서대로 인덱스를 표기한다.
      • 인덱스의 개수는 0 ≤ i < interfaces_count
  • Fields
    • fields_count
      • 현재 클래스 / 인터페이스의 필드 수(클래스 변수, 인스턴스 변수)를 나타낸다.
    • fields[] :
      • 각 원소는 field_info 구조를 따른다.
      • 각 원소는 필드의 Access Flag(접근 권한), 필드명, type, 직렬화 시 포함 여부(transient), 휘발성(volatile), 불변 여부(final), static 여부 등을 나타낸다.
      • 부모 클래스나 부모 인터페이스로부터 상속받은 필드는 필드 테이블 컬렉션에 나열하지 않는다.
field_info {
    u2             access_flags;
    u2             name_index;       // 필드의 단순 이름
    u2             descriptor_index; // 필드 및 메서드 서술자
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}
  • Methods
    • methods_count
      • 메서드 테이블의 갯수를 나타낸다.
    • methods[]
      • 각 원소는 method_info 구조를 따른다.
method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

클래스 속성 정보

final, Exceptions, SourceFile, Generics, Code 등과 같은 클래스 속성 정보를 나타낸다.

  • Attributes
    • attributes_count
    • attributes[]
      • 각 원소는 기본적으로 attribute_info 구조를 따른다.
attribute_info {
    u2 attribute_name_index;
    u4 attribute_length;
    u1 info[attribute_length];
}
Attribute 쓰이는 위치 의미
InnerClasses 클래스 내부 클래스 및 인터페이스에 대한 정보
EnclosingMethod 클래스 내부 클래스가 포함된 메서드에 대한 정보
Synthetic 클래스,필드,메서드 컴파일러가 생성한 클래스를 나타내는 정보
Signature 필드,메서드 제네릭 타입 정보가 포함된 메서드나 클래스의 시그니처
SourceFile 클래스 소스 파일 이름
SourceDebugExtension 클래스 디버깅 정보를 확장하는 데 사용되는 사용자 정의 정보
Deprecated 메서드, 필드 더 이상 사용되지 않는 클래스, 메서드 또는 필드를 나타냄
RuntimeVisibleAnnotations 클래스,메서드,필드 런타임에 보이는 애노테이션
RuntimeInvisibleAnnotations 클래스,메서드,필드 런타임에 보이지 않는 애노테이션
RuntimeVisibleParameterAnnotations 메서드 메서드 파라미터에 대한 런타임에 보이는 애노테이션
RuntimeInvisibleParameterAnnotations 메서드 메서드 파라미터에 대한 런타임에 보이지 않는 애노테이션
AnnotationDefault 메서드 애노테이션 타입 요소의 기본값
BootstrapMethods 클래스 파일 런타임에 동적으로 호출할 메서드에 대한 부트스트랩 메서드 정보
Module 클래스 파일 모듈 시스템 정보를 포함
ModulePackages 클래스 파일 모듈에 포함된 패키지들의 목록
ModuleMainClass 클래스 파일 모듈의 메인 클래스를 지정
NestHost 클래스 파일 현재 클래스의 호스트 클래스를 지정
NestMembers 클래스 파일 현재 클래스의 네스트 멤버 클래스를 지정
Record 클래스 파일 레코드 클래스에 대한 정보
ConstantValue 필드 테이블 final 키워드로 정의된 상수값
Code 메서드 테이블 자바 코드가 컴파일된 결과인 바이트 코드 명령어들
StackMapTable Code속성 스택 프레임의 변화를 나타내는 정보
Exceptions 메서드 테이블 메서드가 던질 수 있는 예외 클래스들
LineNumberTable Code속성 자바 소스 코드의 줄 번호와 바이트 코드 오프셋의 매핑
LocalVariableTable Code속성 메서드의 로컬 변수에 대한 디버그 정보
LocalVariableTypeTable 클래스 로컬 변수의 제네릭 타입 정보
MethodParameters 메서드 테이블 메서드의 매개변수에 대한 정보
  • Code 속성
    • 메서드 본문 코드는 자바 컴파일러에 의해 최종적으로 바이트코드 명령어로 변환된 후 Code 속성에 저장된다.
    • Code_attribute 구조를 따른다.
Code_attribute {    
	u2 attribute_name_index;    // 속성 이름을 가리키는 상수의 인덱스    
	u4 attribute_length;        // 속성의 길이    
	u2 max_stack;               // 피연산자 스택의 최대 깊이    
	u2 max_locals;              // 지역 변수 테이블에 필요한 저장소 공간    
	u4 code_length;             // 바이트코드 스트림 길이    
	u1 code[code_length];       // 바이트코드 스트림    
	u2 exception_table_length;  // 예외 테이블 길이    
	{   
		u2 start_pc;        
		u2 end_pc;        
		u2 handler_pc;        
		u2 catch_type;    
	} exception_table[exception_table_length];  // 예외 테이블    
	u2 attributes_count;    
	attribute_info attributes[attributes_count];
}
  • Exceptions 속성
    • 메서드 테이블의 속성이며, 예외 테이블과는 다르다.
      • JVM에서는 예외 테이블로 핸들링 경로를 정의한다.
    • 메서드에서 throw될 수 있는 예외를 나열하는 기능을 한다.
    • Exceptions_attribute 구조를 따른다.
Exceptions_attribute {    
	u2 attribute_name_index;    
	u4 attribute_length;    
	u2 number_of_exceptions;  // 이 메서드가 던질 수 있는 검사 예외 개수    
	u2 exception_index_table[number_of_exceptions];  // 검사 예외 각각의 타입(상수 풀 인덱스)    
}

ByteViewer

javap 커맨드를 통해 바이트파일 구조를 확인할 수 있다.

다음 샘플 코드를 javap -v -c 옵션을 통해 컴파일하면, 클래스 구조를 확인할 수 있다.

public class ByteCode {
    private static final String field = "string";

    public static String staticMethod() {
        return "staticMethod";
    }

}

바이트 코드 명령어

Java 가상 머신의 명령어는 연산 코드(opcode)와 해당 작업에 필요한 피연산자(0개 이상)으로 이루어진다. 명령어 대부분이 피연산자 없이 연산 코드 하나로 구성되며, 피연산자는 피연산자 스택에 저장된다.

바이트코드 명령어 집합은 연산 코드 길이가 1byte로 제한되기 때문에 최대 256개 연산코드만 표현이 가능하며, 클래스 파일 구조에서는 길이 정렬을 허용하지 않으므로 가상 머신이 런타임에 이 바이트들을 특정 구조로 재구성해야 한다.

자바 가상 머신의 명령어 집합에는 대다수 명령어 자체에 연산에 필요한 데이터 타입의 정보를 명시하고 있다. 대부분 명령어의 연산 코드 이름이 전용 데이터 타입을 뜻하는 문자로 시작한다.

byte, char, short 전용 명령어는 거의 없으며 boolean 타입 전용 명령어는 하나도 없다. 그래서 컴파일러는 컴파일타임이나 런타임에 byte와 short 데이터는 int 타입으로 부호 확장(sign extension)한다.

  • load, store 명령어
    • 스택 프레임의 지역 변수 테이블과 피연산자 스택 사이에서 데이터를 주고받는 데 쓰인다.
    • 데이터를 담는 역할의 피연산자 스택과 지역 변수 테이블은 주로 로드와 스토어 명령어로 조작한다.
    • 객체의 필드나 배열의 원소에 접근할 때도 데이터를 피연산자 스택으로 전송한다.
  • 산술 명령어
    • 피연산자 스택의 값 2개를 이용해 특정 산술 연산을 수행하고, 결과를 다시 피연산자 스택의 맨 위에 저장한다.
    • 데이터를 다루다 보면 오버플로가 발생할 수 있는데, JVM 명세에는 데이터 오버플로우 시 결과에 대한 규정을 명시하지 않고 있다. (런타임 예외 발생 X)
    • 나누는 값이 0이면 ArithmeticException을 던져야 한다.
    • 자바 가상 머신은 비정규화된 부동 소수점 수(denormalized floating-point number)와 점진적 언더플로(gradual underflow) 연산 규칙을 완벽하게 지원해야 한다. (IEEE 754)
      • 가까운 값으로 반올림
        • 모든 연산 결과를 적절한 정밀도로 반올림하고, 정확하지 않은 결과는 표현 가능한 가장 가까운 값으로 반올림한다.
        • 표현 가능한 두 값이 수학적으로 정확한 값과 차이가 똑같다면 최하위 비트가 0인 값을 우선한다.
      • 부동 소수점 수를 정수로 변환 시 0에 가까운 값으로 반올림
  • 형 변환 명령어
    • 숫자 타입 데이터를 다른 숫자 타입으로 변환한다.
    • 데이터 타입 확장시에는 명시적으로 표현하지 않더라도 문제가 없으나, 데이터 타입 축소 변환시에는 명령어를 반드시 명시해야 한다.
    • 축소 변환시 오버플로, 언더플로, 정밀도 손실이 발생할 수 있지만 이에 의해 런타임 예외가 발생하지 않도록 규정하고 있다.
  • 객체 생성 및 접근 명령어
    • 생성된 객체, 배열 인스턴스의 필드 혹은 배열 원소는 객체 접근 명령어를 통해 얻어올 수 있다.
  • 피연산자 스택 관리 명령어
    • 피연산자 스택을 직접 조작할 수 있다.
  • 제어 전이 명령어
    • 프로그램 실행 흐름을 지정한 위치 명령어로 이동시킨다.
    • PC 레지스터의 값을 조건부 / 무조건적으로 변경한다.
  • 메서드 호출 및 반환 명령어
    • invokevirtual: 객체의 인스턴스 메서드를 호출하며, 객체의 실제 타입에 따라 디스패치(가상 메서드 디스패치)한다. 자바 언어에서 가장 많이 쓰이는 메서드 디스패치 방식이다.
    • invokeinterface: 인터페이스 메서드를 호출하며, 런타임에 이 인터페이스 메서드를 구현한 객체를 검색하여 적절한 메서드를 찾는다.
    • invokespecial: 인스턴스 초기화 메서드, private 메서드, 부모 클래스의 메서드를 포함하여 특수 처리가 필요한 일부 인스턴스 메서드를 호출한다.
    • invokestatic: 클래스 메서드(static 메서드)를 호출한다.
    • invokedynamic: 런타임에 호출 사이트 한정자가 참조하는 메서드를 동적으로 찾아 호출한다. 앞의 4개 호출 명령어의 디스패치 로직은 사용자가 변경할 수 없으나, 이 명령어의 디스패치 로직은 가상 머신 실행시 사용자가 설정 가능하다.
  • 예외 처리 명령어
    • throw로 명시적 예외를 던지는 경우, athrow 명령어로 구현된다.
    • 명시적 예외 외에도 비정상적 상황을 만나면 예외를 던질 수 있다. 이전에는 예외 처리를 jsr / ret으로 구현했으나 이 방식은 더이상 사용되지 않으며, 예외 테이블로 핸들링 경로를 정의한다.
  • 동기화 명령어
    • Java에서는 메서드 수준 동기화와 명령어 블록 동기화를 지원하며, 두 가지 방식 모두 구현에 모니터를 사용한다.
    • 메서드 수준 동기화는 바이트코드 명령어가 아니라 메서드 호출과 반환 명령어로 구현된다.
    • 명령어 블록 동기화는 일반적으로 synchronized {}로 표현되며, moniterenter, monitorexit 명령어로 구현된다.

출처

Leave a comment