Chapter 6. Class file structure
Last updated
Last updated
플랫폼 독립성은, 자바 가상 머신이 다양한 플랫폼을 지원하고, 모든 가상 머신이 동일한 프로그램 저장 형식(바이트코드)을 지원한다는 것.
다만, 자바 가상 머신은 가상 머신에서 다른 언어를 실행할 가능성을 염두해둔 "언어 독립성"도 보장하려고 노력중.
언어가 어떻든, 가상 머신을 통해 최종 "클래스 파일"로 저장합니다.
모든 클래스 파일은 각각 하나의 클래스 또는 인터페이스를 정의함. 하지만, 클래스나 인터페이스를 꼭 파일에 담아둘 필요는 없는데, 동적으로 생성하여 클래스 로더에 직접 제공할 수 있기 떄문.
클래스 파일은 바이틀르 하나의 단위로 하는 이진 스트림 집합체(0,1)이다.
각 데이터 항목이 정해진 순서에 맞게, 구분 기호 없이 조밀하게 나열됨.
파일 전체가 낭비되는 공간 없이 프로그램 실행을 위한 데이터로 채워진다.
1 바이트가 넘는 데이터 항목은 바이트 단위로 분할됨.
<자바 가상 머신 명세>에 따르면, 데이터를 저장하는 데는 의사 구조(pseudo structure)를 이용함.
같은 타입의 데이터를 여러개 표현할때, 개수가 정해지지 않았다면, *_count 형태의 항목들을 통해 개수를 알려줌. {개수 + 개수만큼의 데이터 타입} 형태를 해당 타입의 "컬렉션"이라고 함
클래스 구조는 XML 같은 언어를 이용하지 않고, 구분자가 없기떄문에 데이터 항목은 데이터가 저장되는 바이트 순서, 의미, 길이, 순서가 엄격하게 제한됨.
클래스 파일의 바이트 순서는 빅 엔디언.
1~4번째 바이트: 모든 클래스 파일의 처음 4바이트는 가상 머신이 허용하는 클래스 파인인지 여부를 빠르게 확인하는 매직 넘버로 시작함. 0xCAFEBABE. (클래스 파일뿐만 아니라, GIF, JPEG 같은 파일에도 파일 헤더에 매직 넘버 등장)
5~6번째 바이트: 마이너 버전
7~8번째 바이트: 메이저 버전
45번쨰 바이트 부터 자바 버전 번호가 나옵니다.
버전 번호 다음으로는 상수 풀 항목입니다. 상수 풀은 클래스 파일의 자원 창고라고 하며, 클래스 파일 구조에서 다른 클래스와 가장 많이 연관된 부분입니다. 차지하는 공간도 대체로 가장 큽니다.
상수 풀에 들어 있는 상수의 수는 고정적이지 않으므로, 상수 풀 항목들에 앞서 항목 개수를 알려주는 u2 타입 데이터가 필요. 관례상 개수는 1부터 시작.
위 그림에서 TestClass 클래스의 상수 풀 크기는 16진수로 0x0013이고, 10진수 19에 해당. 다시 말해, 상수 풀에는 상수가 18개 존재하며, 인덱스 범위는 1~18까지다.
0번째 상수를 비운 이유: '상수 풀 항목을 참조하지 않음'을 표현해야하는 특수한 경우에 인덱스를 0으로 설정하도록 함.
클래스파일에서 상수풀만 개수를 1부터 시작.
상수풀에 담기는 유형은 리터럴과 심벌 참조.
자바 코드를 javac로 컴파일 할때는 링크 단계가 없다. 필드와 메서드가 메모리에서 어떤 구조로 표현되는가에 대한 정보는 클래스 파일에 저장되지 않음. (자바에서 링크는 가상 머신이 클래스 파일을 로드할 때 동적으로 이루어짐.)
가상 머신은 클래스 파일을 로드할 때 상수 풀에서 해당 심벌 참조들을 가져옴. 그 뒤, 클래스가 생성되거나 구동할 때 해석하여 실제 메모리 주소로 변환.
상수 풀 안의 상수 각각이 모두 테이블입니다. JDK 21 기준으로 총 17가지 상수 타입이 존재합니다. (초기에 11가지 테이블 구조 + 동적언어 지원을 위한 4가지 + 모듈 시스템을 지원하기 위한 2가지)
공통적으로 u1 타입의 플래그 비트로 시작하며, 현재 상수가 속한 상수 타입을 나타냄.
상수풀 테이블 구조 예시:
예시1: 값이 10인 상수 타입은 CONSTANT_Methodref_info 입니다.
같은 클래스의 메서드를 가리키는 심벌 참조이며 구조는 아래와 같습니다.
tag: 플래그 비트. 상수 타입을 구분하는 용도
index: 상수 풀에서의 인덱스로 위에서는 각각 2와 3입니다. 전체 의미를 파악하기 위해서는 2번쨰, 3번째 상수도 확인해봐야 함.
이 타입은 클래스나 인터페이스를 가리키는 심벌 참조이며 구조는 아래와 같습니다.
name_index의 값은 4 이므로, 네번쨰 상수를 보면 해당 메서드가 정의된 클래스의 이름을 알 수 있습니다. 하나씩 추적해서 조합하면, 첫번째 상수의 의미는 Object 클래스의 기본 인스턴스 생성자임을 알 수 있습니다.
그림의 분석 결과
#4: 클래스 이름
#5: 메서드 이름
#6: 메서드의 타입
표 6-3에서 확인해보면 CONSTANT_utf8_info 타입 상수이며 구조는 아래와 같습니다.
length: UTF-8 축약 인코딩된 문자열이 몇 바이트인지 나타냄. 바로 이어서 길이만큼 데이터가 문자열의 실제 데이터.
현재 예시에서 이름이 되는 문자열의 길이(오프셋: 0x00000018)는 0x0010, 즉 16바이트임.
이어지는 16바이트는 "java/lang/Object"이다.
그 외에 나머지 12개의 상수 풀의 상수는 javap로 출력한 내용을 참조.
상수 풀듸 다음 2바이트는 현재 클래스의 접근 정보를 식별하는 접근 플래그입니다. 현재 클래스 파일이 표현하는 대상이 클래스/인터페이스/public/abstract인지, 클래스의 경우 final인지 등의 정보가 담김니다.
이어서 현재 클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스 컬렉션(interfaces)이 나옵니다. 파일의 상속 관계를 규정.
클래스 인덱스, 부모 클래스 인덱스
u2타입
현재 클래스와 부모 클래스의 완전한 이름을 결정하는데 사용.
최상위 클래스 java.lang.Object를 제외한 모든 자바 클래스 인덱스는 값이 0이 될 수 없음.
인터페이스 인덱스 컬렉션
u2 타입 데이터들의 묶음
현재 클래스가 구현한 인터페이스들을 기술.
컬렉션의 첫 항목은 테이블 크기를 뜻함. 0이면 아무런 인터페이스도 구현하지 않았다는 뜻.
인터페이스나 클래스 안에 선언된 변수들을 설명하는데 사용됩니다. 여기서 필드란, 클래스 변수와 인스턴스 변수를 뜻함. (메서드 안에 선언된 지역 변수는 필드가 아님)
필드에 포함되는 정보
public, private protected: 필드에 접근 범위 제한
static: 인스턴스 변수와 클래스 변수의 구분
final: 불변 여부
volatile: 휘발성
transient: 직렬화 시 포함 여부
데이터 타입: 기본 타입, 객체, 배열
필드 이름
위 정보중 modifier는 각각 true/false로 나타낼 수 있음. 반면 필드 이름과 타입은 상수 풀에 정의된 상수를 참조해야함
필드 테이블 구조 예시:
코드를 컴파일해 생성한 TestClass.class 파일의 경우 테이블 컬렉션이 0x000000B9부터 시작.
0x0001: 필드 개수를 뜻하는 field_count. 필드 테이블에는 데이터가 단 하나라는 뜻.
0x0002: accessFlag 이며, ACC_PRIVATE플래그만 true.
0x000B: 필드 이름을 가르키고, 상수풀 11번째 상수를 확인하면 CONSTANT-Utf8_info 타입의 문자열이고, 값은 "m".
0x000C: 필드 서술자이며, 상수풀에서 찾아보면 문자열 "I".
조합하면 private int m임을 알 수 있다.
만약 final static int m = 123; 이라면, 123을 가리키는 ConstantValue 속성이 등장했을것
내부 클래스는 외부 클래스를 가리킬 수단으로, 소스코드에서는 존재하지 않는 필드가 등장할 수도 있음
필드 테이블과 유사하지만, volatile과 acc_transient가 사라지고, synchronized, native, strictfp, abstract 키워드에 대응하는 플래그가 추가되었습니다.
메서드 정의는 접근 플래그, 이름 인덱스, 서술자 인덱스만으로 명확하게 표현 가능.
메서드 본문의 코드는 javac 컴파일러에 의해 바이트코드 명령어로 변환된 후, 메서드 속성 테이블 컬렉션의 "Code" 속성에 따로 저장됨.
클래스 파일, 필드 테이블, 메서드 테이블, Code 속성, 레코드 구성요소(record_component_info)는 모두 특정 시나리오에서 특정한 정보를 설명하기 위해 고유한 속성 테이블을 포함할 수 있음.
다른 데이터 항목들과 다르게, 속성 테이블 컬렉션은 제약이 상대적으로 느슨하며, 순서에도 엄격하지 않음
기존 속성 이름과 중복되지 않는 한, 자체 제작한 컴파일러가 새로운 속성 정보를 속성 테이블에 추가할 수 있도록 허용
자바 가상 머신이 인식하지 못하는 속성은 무시.
속성 이름은 모두 CONSTANT_Utf8 타입 상수를 참조해 표현.
속성 값은 u4 타입으로 나타냄.
속성값 자체는 사용자 정의할 수 있다. 다만 속성 테이블이 만족해야하는 공통 구조는 아래와 같음
자바 프로그램의 메서드 본문 코드는 자바 컴파일러에 의해 최종적으로 바이트코드 명령어로 변환된 후 Code 속성에 저장됨. (Code 속성은 메서드 테이블의 속성 컬렉션에 위치하지만, 모든 메서드 테이블에 포함되는 것은 아니다. 인터페이스 / 추상클래스의 추상 메서드는 code에 없음).
테이블의 code 속성과 같은 맥락. ( ≠ 예외 테이블)
메서드에서 throw될 수 있는 검사 예외들을 나열하는 기능. (메서드 설명에서 throws 키워드 뒤에 나오는 예외들)
자바 소스코드의 줄 번호와 바이트코드의 줄 번호(바이트코드 오프셋) 사이의 대응 관계를 설명하는 속성
프로그램을 실행하는 데 꼭 필요한 속성은 아니지만, 클래스 파일에 기본적으로 생성됨.
해당 속성이 없으면, 프로그램에서 예외가 발생했을 때 오류를 일으킨 코드의 줄 번호가 스택 추적 정보에 나타자이 않음.
LocalVariableTable
스택 프레임에 있는 지역 변수 테이블 안에 변수와 자바 소스 코드에 정의된 변수 사이의 관계를 설명하는 속성
이 속성을 새성하지 않으면, 다른 사람이 이 메서드를 참조할 때 매개 변수 이름을 알 수 없음
LocalVariableTypeTable
제네릭이 도입되면서 자매 속성으로 추가됨.
SourceFile
클래스 파일을 생성한 자바 소스 파일 이름이 기록.
SourceDebugExtention
컴파일러에 의해 또는 동적으로 생성된 클래스에 개발자를 위한 사용자 정보를 쉽게 추가할 수 있도록 설계된 속성.
정적 변수에 값을 자동으로 할당하도록 가상 머신에 알립니다.
static 키워드로 선언된 변수(클래스 변수)에만 이 속성이 붙습니다.
내부 클래스와 호스트 클래스 사이의 연결 관계를 기록합니다. 내부 클래스를 정의하면 컴파일러가 InnerClasses 속성을 자동으로 생성합니다.
Deprecated
클래스, 필드 또는 메서드를 프로그램 작성자가 폐기 대상으로 지정했음을 나타냄. (@deprecated)
Synthetic
컴파일러가 추가한 필드나 메서드임을 나타냄.
JDK5부터는 접근 플래그에 ACC_SYNTHETIC을 설정하여 컴파일러가 자동 생성해 추가한 필드과 메서드를 식별할 수 있음
JDK 6 때 클래스 파일 명세에 추가됨.
Code 속성의 attributes 테이블에 자리하는 복잡한 가변 길이 속성.
가상 머신이 클래스를 로드할 때 바이트코드 검증 단계에서 타입 검증기가 활용함
JDK 5때 제네릭을 지원하기 위해 추가됨
클래스의 속성 테이블, 필드 테이블, 메서드 테이블에 선택적으로 등장할 수 있으며, 길이는 일정함
클래스, 인터페이스, 초기화 메서드, 기타 클래스 멤버가 타입 변수나 매개 변수화 타입을 포함할 경우 제네릭 시그니처 정보를 담기 위해 이용.
자바 언어가 제네릭을 소거법으로 구현했기때문에 이 속성이 필요함.
JDK 7떄 추가됨. 복잡한 가변 길이 속성으로, 클래스 파일의 속성 테이블에 위치.
invokedynamic 명령어가 참조하는 부트스트랩 메서드 한정자가 담김.
메서드 테이블에서 사용되는 가변 길이 속성.
메서드가 받는 매개 변수 각각의 이름과 정보를 기록.
모듈 관련 기능을 지원하기 위해 클래스 파일 형식도 확장하여 Module, ModulePackages, ModuleMainClass 속성을 추가.
모듈 이름, 버전, 플래그 정보와, 모듈에 정의된 requirements, exports, opens, uses, provides 요구 사항의 내용을 모두 담음.
애너테이션 정보를 담기 위한 새로운 네가지 속성
RuntimeVisibleAnnotations, RuntimeInvisitibleAnnotations, RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations.
불변 객체를 쉽게 생성할 수 있도록 해주는 클래스.
JDK 16부터 도입
봉인된 클래스(봉인된 인터페이스)를 지원하기 위한 속성
자바 가상 머신의 명령어는 특정 작업을 뜻하는 바이트 길이의 숫자인 연산 코드(opcode)와 해당 작업에 필요한 0개 이상의 피연산자로 이루어짐. 피연산자는 피연산자 스택에 저장됨.
바이트 코드 명령어 집합은, 고유한 특징과 장단점이 있는 명령어 집합 아키텍처입니다.
단점:
연산 코드 길이가 1바이트로 제한되기 떄문에 최대 256개의 연산 코드만 표현 가능
클래스 파일 구조에서는 컴파일된 코드에 들어있는 피연산자의 길이 정렬을 허용하지 않음 -> 따라서,1바이트가 넘는 데이터를 처리할때는 가상 머샤니이 런타임에 해당 바이트들을 특정 구조로 재구성해야함. -> 결론적으로, 바이트코드를 해석하고 실행하는 속도가 조금 느려짐.
장점:
피연산자 길이 정렬을 포기하여, 패딩과 공백을 없앨 수 있음.
연산 코드들이 바이트 하나로 표현되기 때문에 컴파일된 결과물이 짧고 간결.
자바 가상 머신 명령어 집합을 보면 대다수 명령어 자체에 해당 연산에 필요한 데이터의 타입 정보가 포함되어 있습니다. (예를 들어, iload 명령어는 지역 변수 테이블에서 피연산자 스택으로 int 타입 데이터를, float 명령어는 float 타입 데이터를 읽어들임.)
데이터 타입과 관련된 대부분의 바이트 코드 명령어는 연산 코드 이름이 전용 데이터 타입을 뜻하는 문자로 시작됩니다. (iload는 int 타입 데이터 연산을 뜻함. arraylength나 goto 같은 경우는 예외)
1바이트 크기의 연산 코드에 데이터 타입 정보까지 포함시키기엔 어려워서, 자주 쓰이는 연산과 데이터 타입 조합에만 전용 명령어를 배정했고, 그 외 타입은 별도 지시문을 이용해서 지원되는 타입으로 변환해 사용합니다.
스택 프레임의 지역 변수 테이블과 피연산자 스택 사이에서 데이터를 주고 받는데 사용됨.
데이터를 담는 역할의 피연산자 스택과 지역 변수 테이블이 해당 명령어들로 조작됩니다.
피연산자 스택의 값 두개를 이용해 특정한 산술 연산을 수행하고, 결과값을 다시 피연산자 스택의 맨 위에 저장합니다.
정수 데이터를 다루는 부류와, 부동 소수점 데이터를 다루는 부류로 구분됨.
정수 데이터:
데이터를 다루다 0으로 나누는 경우네는 ArithmeticException을 던져야한다는 것은 규정했지만, 오버플로가 나는 경우 어떤 결과를 내야하는지는 명시되지 않습니다.
부동 소수점 데이터:
자바 가상 머신은 IEEE754가 정의한 비정규화된 부동 소수점 수와 점진적 언더플로 연산 규칙을 완벽하게 지원해야합니다.
반올림 모드 규칙:
모든 연산 결과를 적절한 정밀도로 반올림.
정확하지 않은 결과는 표현 가능한 가장 가까운 값으로 반올림.
표현 가능한 두 값이 '수학적으로 정확한 값'과 차이가 똑같다면, 최하위 비트가 0인 값을 우선시.
숫자 타입 데이터를 다른 숫자 타입으로 변환합니다.
데이터 타입의 표현 범위가 넓어지는 경우: 자바 가상 머신이 알아서 수행합니다.
표현 범위가 축소되는 경우: 형 변환 명령어를 반드시 명시해야합니다. (변환 값이 부정확해지기 때문에)
프로그램의 실행 흐름을 조건에 따라 지정한 위치의 명령어로 이동시킵니다.
int 타입용 조건 분기 명령어와, 참조 타입용 조건 분기 명령어가 따로 있음
null 값 확인용 명령어도 별도로 제공
boolean, byte, char, short 타입 조건분기에는 모두 int 타입용 명령어를 사용
long, float, double 타입의 조건 분기에는 각 타입 전용의 비교 연산 명령어를 먼저 실행
throw 문으로 예외를 명시적으로 던진은 작업은 athrow 명령어로 구현됨.
자바 가상 머신은 예외 처리를 바이트코드 명령어 대신 예외 테이블을 이용해 구현합니다.
메서드 수준 동기화
메서드 수준 동기화는 바이트코드 명령어가 아니라 메서드 호출과 반환 명령어로 구현됨.
상수풀-메서드 테이블에 있는 ACC_SYNCHRONIZED 접근 플래그를 확인하여 알 수 있음
명령어 블록 동기화
minotirenter와 monitorexit 명령어가 사용됨.
자바 가상 머신 명세는 공통된 프로그램 저장 형식(클래스 파일 형식과 바이트코드 명령어 집합)을 정의합니다. 이는 어떤 하드웨어나 운영체제를 사용하든 상관없이 지켜져야하는 약속입니다.
다만, 어떻게 구현을 했는냐는 <자바 가상 머신 명세>에 따라 구현되지 않아도 됩니다. 가상 머신 구현자는 확장성을 활용하여, 고성능, 적은 메모리 소비, 훌륭한 이식성을 갖춘 가상 머신을 구현할 수 있습니다. 어떤 측면에 집중할지는 구현 목표에 따라 다르지만 주로 다음 두 가지로 귀결됩니다.
로딩 시 또는 런타임에 자바 가상 머신 코드를 다른 가상 머신용 명령어 집합으로 변환.
로딩 시 또는 런타임에 자바 가상 머신 코드를 호스트 CPU의 네이티브 명령어 집합으로 변환(JIT 컴파일러의 코드 생성 기술)