모든 클래스 파일은 각각 하나의 클래스 또는 인터페이스를 정의한다.(단, package-info.class와 module-info.class는 특정 클래스나 인터페이스를 정의하지 않는다. 대신 해당 패키지와 모듈에 대한 정보를 기술)
반면 클래스나 인터페이스를 꼭 파일에 담아둘 필요는 없다. 동적으로 생성하여 클래스 로더에 직접 제공할 수도 있다.
일단 클래스나 인터페이스가 따라야 하는 형식을 '클래스 파일 형식'이라고 지칭하자
클래스나 인터페이스가 디스크에 파일 형태로 존재할 필요는 없다.
클래스 파일은 바이트를 하나의 단위로 하는 이진 스트림 집합체다.
각 데이터 항목이 정해진 순서에 맞게, 구분 기호 없이 조밀하게 나열 => 클래스 파일 전체가 낭비되는 공간 없이 프로그램을 실행하는데 꼭 필요한 데이터로 채워진다.
1바이트가 넘는 데이터 항목은 바이트 단위로 분할, 이 때 큰 단위의 바이트가 먼저 저장되는 빅 엔디언 방식으로 표현된다.
빅 엔디언 방식은 가장 큰 단위의 바이트가 가장 낮은 주소에, 가장 작은 단위의 바이트가 가장 높은 주소에 저장된다. 그 반대 방식은 리틀 엔디언이라고 한다.
<<자바 가상 머신 명세>>에 따르면 클래스 파일에 데이터를 저장하는 데는 C언어의 구조체와 비슷한 의사 구조를 이용
이 의사 구조에는 '부호 없는 숫자'와 '테이블'이라는 두 가지 데이터 타입만 존재한다.
부호 없는 숫자: 기본 데이터 타입을 표현
u1, u2, u4, u8은 각각 1바이트, 2바이트, 4바이트, 8바이트 의미
숫자, 인덱스 참조, 수량값을 기술하거나 UTF-8로 인코딩된 문자열 값을 구성
테이블 : 복합 데이터 타입(여러 개의 부호 없는 숫자나 또 다른 테이블로 구성) 표현
구분이 쉽도록 테이블 이름은 관례적으로 '_info'로 끝난다.
테이블은 계층적으로 구성된 복합 구조의 데이터를 설명하는 데 사용
클래스 파일 전체는 본질적으로 테이블이다.
// 클래스 파일 구조
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
같은 타입의 데이터 여러 개를 표현할 때 그 개수가 정해져 있지 않다면 개수를 알려 주는 타입이 바로 앞에 등장한다. '*_count' 형태의 항목들이 여기 속한다.
이처럼 {개수 + 개수만큼의 데이터 타입} 형태를 해당 타입의 '컬렉션'이라고 한다.
클래스 구조는 XML 같은 언어를 이용하지 않는다.
구분자가 없기 때문에 위의 클래스 구조 정의의 데이터 항목은 데이터가 저장되는 바이트 순서, 각 바이트의 의미, 길이, 순서가 모두 엄격하게 제한되며 변경 불가하다.
클래스 파일의 바이트 순서는 빅 엔디언이다.
매직 넘버와 클래스 파일의 버전
모든 클래스 파일의 처음 4바이트는 매직 넘버로 시작한다.
매직 넘버는 가사 머신이 허용하는 클래스 파일인지 여부를 빠르게 확인하는 용도로만 쓰인다.
클래스 파일의 매직 넘버는 0xCAFEBABE다.
매직 넘버 다음의 4바이트는 클래스 파일의 버전 번호다.
5 ~ 6번째 바이트는 마이너 버전을, 7 ~ 8번째 바이트는 메이저 버전을 뜻한다.
자바 버전 번호는 45부터 시작, JDK 1.1 이후 주요 JDK 릴리스의 메이저 버전은 1씩 증가(JDK 1.0 ~ 1.1은 45.0 ~ 45.3 사용)
상위 버전 JDK는 하위 버전을 인식할 수 있지만 하위 버전 JDK에서 상위 버전의 클래스 파일을 실행할 수는 없다.
<<자바 가상 머신 명세>>의 '클래스 파일 검증' 절에서 파일 형식이 변경되지 않았더라도 상위 버전의 클래스 파일을 실행하면 안 된다고 규정
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
HxD 십육진수 편집기로 실제 클래스 파일의 기계어 코드를 볼 수 있다.
첫 번째 4바이트에서 0xCAFEBABE 값을 확인할 수 있다.
5~6번째 바이트는 마이너 버전인 0x0000이고, 이어서 메이저 버전인 0x003D가 이어진다.
십육진수 3D는 십진수로 61이다. 이 버전 번호로부터 이 클래스 파일은 JDK 17 이상의 가상 머신에서 실행할 수 있음을 알 수 있다.
마이너 버전 번호는 JDK 1.x 이전에 잠시 사용되었다.
상수 풀
버전 번호 다음은 상수 풀 항목이다.
상수 풀은 클래스 파일의 자원 창고로, 클래스 파일 구조에서 다른 클래스와 가장 많이 연관된 부분이다.
차지하는 공간도 대체로 가장 크다.
상수 풀에 들어있는 상수의 수는 고정적이지 않으므로 상수 풀 항목들에 앞서 항목 개수를 알려주는 u2 타입 데이터가 필요
TestClass의 상수 풀 크기는 십육진수로 0x0016으로 십진수로는 22에 해당, 상수 풀에는 상수가 21개 존재하며 인덱스 범위는 1~21까지다.
클래스 파일 형식 설계자가 0번째 상수를 비운 이유가 있다.
상수 풀 인덱스를 가리키는 데이터에서 '상수 풀 항목을 참조하지 않음'을 표현해야 하는 특수한 경우에 인덱스를 0으로 설정하도록 한 것이고 그 외에 인덱스 컬렉션, 필드 테이블 컬렉션, 메서드 테이블 컬렉션 등의 원소 개수는 모두 0부터 센다.
상수 풀에 담기는 상수 유형은 리터럴과 심벌 참조 두 가지
리터럴은 자바 언어 수준에서 이야기 하는 상수(final로 선언된 문자열이나 상수)
심벌 참조는 컴파일과 관련된 개념으로 다음 유형의 상수들 포함
필드 이름과 서술자
메서드 이름과 서술자
메서드 핸들과 메서드 타입
클래스와 인터페이스의 완전한 이름
모듈에서 익스포트하거나 임포트하는 패키지
동적으로 계산되는 호출 사이트와 동적으로 계산되는 상수
자바 코드를 컴파일할 때는 C, C++과 달리 링크 단계가 없다.
자바에서 링크는 가상 머신이 클래스 파일을 로딩하는 JVM 런타임에 동적으로 이루어진다.
가상 머신이 필드와 메서드의 심벌 참조를 JVM 런타임에 변환하지 않으면 각 항목의 실제 메모리 주소를 알 수 없기 때문에
가상 머신은 클래스 파일을 로드할 때 상수 풀에서 해당 심벌 참조들을 가져 온다.
그 후 클래스 타입의 인스턴스가 생성되거나 구동할 때 해석하여 실제 메모리 주소를 변환한다.
상수 풀 안의 상수 각각은 모두 테이블이다.
JDK 21 기준 총 17가지 상수 타입이 존재한다.
17가지 타입의 테이블들은 공통적으로 u1타입의 플래그 비트로 시작하며, 그 값은 현재 상수가 속한 상수 타입을 나타낸다.
17가지 상수 타입 각각의 데이터 구조는 독립적이다.
상수 풀의 항목 타입
상수풀 항목 이름
Tag 값
주요 용도
데이터 구조/포함 정보
CONSTANT_Utf8_info
1
문자열 저장 (메서드명, 필드명 등)
길이 + UTF-8 바이트
CONSTANT_Integer_info
3
int 상수 값
4바이트 정수 값
CONSTANT_Float_info
4
float 상수 값
4바이트 IEEE 754 float 값
CONSTANT_Long_info
5
long 상수 값
8바이트 정수 값
CONSTANT_Double_info
6
double 상수 값
8바이트 IEEE 754 double 값
CONSTANT_Class_info
7
클래스/인터페이스 이름 참조
이름에 대한 Utf8 상수풀 인덱스
CONSTANT_String_info
8
String 리터럴 참조
Utf8 상수풀 인덱스
CONSTANT_Fieldref_info
9
필드 참조
Class 인덱스 + NameAndType 인덱스
CONSTANT_Methodref_info
10
메서드 참조
Class 인덱스 + NameAndType 인덱스
CONSTANT_InterfaceMethodref_info
11
인터페이스 메서드 참조
Class 인덱스 + NameAndType 인덱스
CONSTANT_NameAndType_info
12
이름과 타입 서명 정보
이름 인덱스 + 타입 서명 인덱스
CONSTANT_MethodHandle_info
15
메서드 핸들 (invoke dynamic 관련)
참조 kind + 참조 index
CONSTANT_MethodType_info
16
메서드 타입 정보
Descriptor(Utf8) 인덱스
CONSTANT_Dynamic_info
17
동적 상수 (런타임 시점에 결정)
Bootstrap method index + NameAndType 인덱스
CONSTANT_InvokeDynamic_info
18
동적 메서드 호출
Bootstrap method index + NameAndType 인덱스
CONSTANT_Module_info
19
모듈 이름
Utf8 인덱스
CONSTANT_Package_info
20
패키지 이름
Utf8 인덱스
TestClass의 첫 번째 상수의 플래그 비트는 0x0A이다. 십진수로 10
상수 풀의 항목 타입에서 플래그 값이 10인 상수 타입은 CONSTANT_Methodref_info이다.
이 타입은 '같은 클래스의 메서드'를 가리키는 심벌 참조다.
CONSTANT_Methodref_info의 구조
CONSTANT_Methodref_info {
u1 tag;
u2 index;
u2 index;
}
TestClass의 첫 번째 상수의 첫 번째 인덱스와 두 번째 인덱스 모두 상수 풀에서의 인덱스로, 값이 각각 2와 3이다.
상수 풀에서의 두 번째 상수와 세 번째 상수를 확인해봐야 한다.
두 번째 상수의 플래그 비트(오프셋 주소: 0x0000000F)는 07이므로 상수 타입은 CONSTANT_Class_info이다.
이 타입은 '클래스나 인터페이스를 가리키는 심벌 참조'
CONSTANT_Class_info의 구조
CONSTANT_Class_info {
u1 tag;
u2 name_index; // 0004
}
세 번째 상수의 플래그 비트(오프셋 주소: 0x00000012)는 0C이므로 상수 타입은 CONSTANT_NameAndType_info이다.
이 타입은 '메서드의 이름과 타입을 가리키는 심벌 참조'
CONSTANT_NameAndType_info의 구조
CONSTANT_NameAndType_info{
u1 tag;
u2 index; // 0005
u2 index; // 0006
}
상수 풀에서 두 번째 상수의 name_index의 값은 04이므로 상수 풀에서의 네 번째 상수를 보면 이 메서드가 정의된 클래스의 이름을 알 수 있게 된다.
이런 식으로 하나씩 추적해 조합하면 첫 번째 상수의 의미는 Object 클래스의 기본 인스턴스 생성자임을 알게 된다.
추적하는 과정에서 클래스의 이름(상수 풀에서 네 번째 상수 데이터 구조 안), 메서드 이름(상수 풀에서 다섯 번째 상수 데이터 구조 안), 메서드 타입(상수 풀에서 여섯 번째 상수 데이터 구조 안) 정보는 모두 플래그 비트가 0x01로, UTF-8로 인코딩된 문자열로 표현된다.
플래그 비트가 01에 해당되는 상수 타입은 CONSTANT_Utf8_info이다.
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length]
}
클래스 이름이 되는 문자열의 길이(오프셋 주소: 0x00000018)는 0x0010, 즉 16바이트다. 이어지는 16바이트는 모두 아스키코드로 1~127 범위에 있으며, 값은 "java/lang/Object"다.
JDK의 bin 디렉터리 안의 javap 실행 파일은 클래스 파일의 바이트코드 분석 도구다.