Be-Developer

JVM 밑바닥까지 파헤치기 : 6 클래스 파일 구조

6. 클래스 파일 구조

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
00000000    CA  FE  BA  BE  00  00  00  3D  00  13  0A  00  02  00  03  07  ...  

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

매직넘버

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
00000000    CA  FE  BA  BE  

u4 magic

  • 가상머신이 허용하는 클래스 파일인지 여부를 빠르게 확인하는 용도.
  • 확장자와 달리 수정불가해서 안정적.
  • .class 파일의 매직넘버는 0xCAFEBABE

클래스 파일 버전

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
00000000    --  --  --  --  00  00  00  3D
## 십진수                      0   0   0  61
  • 04 05가 minor version : 0
    • u2 minor_version
    • JDK 1.2 부터는 minor version을 사용하지않아 모두 0,
    • 테스트 기능사용 버전인 경우 65535로 지정됨.
  • 06 07이 major version : 61
    • u2 major_version
    • JDK 1.1 버전 = 45, 1씩 추가되며 61은 JDK 17

2. 상수풀

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
00000000    --  --  --  --  --  --  --  --  00  13
## 십진수                                      0  19
  1. 상수풀 항목의 갯수 u2 constant_pool_count
    • 값 0 : 상수풀 항목을 참조하지 않음
    • 카운트는 1부터 시작함. 여기서는 상수 18개 존재
      Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
      00000000    --  --  --  --  --  --  --  --  --  --  0A  00  02  00  03
      ## 십진수                                             10   0   2   0   3 
      
  2. 상수풀 테이블 컬렉션 cp_info constant_pool[constant_pool_count-1]
    • 상수풀 안의 상수 각각이 모두 테이블이다.
    • JDK 21 기준으로 17가지의 상수타입이 존재하며, 모두 구조가 다르다.
    • 첫번째 상수 : 0A ~ 0E 상수
      • 0A : u1 tag :flag bit, 10=CONSTANT_Methodref_info = 같은 클래스의 메서드를 가리키는 심벌.
      • 0B 0C : u2 index 2 = 두번째 상수도 확인해봐야한다.
      • 0D 0E : u3 index 3 = 세번째 상수도 확인해봐야한다.
         Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
         00000000    --  --  --  --  --  --  --  --  --  --  --  --  --  --  --  07
         00000010    00  04
         ## 십진수      0   4                                                       7
        
    • 두번째 상수 : 0F ~ 01 상수
      • 0F : u1 tag :flag bit, 7=CONSTANT_Class_info = 클래스나 인터페이스를 가리키는 심벌 참조.
      • 00 01 : u2 name_index : 4 = 네번째 상수에 클래스 이름을 알 수 있다.
    • 세번째 상수 : 02 ~ … 상수 위와 동일한 방식으로 해석.
  • 자바 클래스 이름의 길이가 결국 u2 length 가 표현할 수 있는 길이로 한정된다. = 65535 = 영문자 65535자
    • = 변수나 메서드 이름의 길이가 64KB를 넘으면 컴파일되지 않는다.

javap

class 파일의 바이트코드 분석 도구.

$ {JAVA_HOME}/bin/javap -verbose TestClass
  • 결과중에 소스코드에서는 없던 Utf8 상수가 있는데, (I,()V,<init>..) 이것들은 컴파일러가 자동 생성한 것으로 메서드의 반환값, 매개변수 갯수, 타입 등을 설명하는데 사용된다.

3. 접근 플래그 (access_flag)

  • 상수 풀 다음 2byte
  • 현재 클래스의 접근 정보를 식별하는 플래그.
  • public, final, interface, abstract, enum, module,, 여부 등
    public class TestClass {}
    
    Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
    000000B0    --  00  21
    
  • 0x0021 = 0x0001 0x0020 = ACC_PUBLIC & ACC_SUPER
    • ACC_PUBLIC 0x0001 = true : public type
    • ACC_FINAL 0x0010 = false : final X
    • ACC_SUPER 0x0020 = true : JDK1.2 이상 사용.

4. 클래스 인덱스(this_class), 부모 클래스 인덱스(super_class), 인터페이스 인덱스 컬렉션(interfaces)

클래스 인덱스

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
000000B0    --  --  --  00  08 
  • 현재 클래스의 완전한 이름을 결정
  • 8번째 상수가 클래스 이름을 가지고 있다. (CONSTANT_Class_info)

부모 클래스 인덱스

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
000000B0    --  --  --  --  --  00  02 
  • java.lang.Object를 제외한 모든 자바 클래스는 부모클래스 인덱스값이 0이 아니다.
  • 부모 클래스의 완전한 이름을 결정.
  • 2번째 상수가 부모클래스 정보를 가지고 있다. (CONSTANT_Class_info)

인터페이스 인덱스 컬렉션

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
000000B0    --  --  --  --  --  --  --  00  00 
  • u2 length 컬렉션 사이즈 = 0 = 구현중인 인터페이스가 없다.
  • 인터페이스가 있었으면 그 뒤로 주소 상수 인덱스가 나왔을 것.

5. 필드 테이블 (field_info)

  • 자바언어에서 필드는 지역변수를 포함하지 않는다. 클래스 필드, 인스턴스 변수를 지칭.
  • u2 access_flags : class의 access_flag와 유사.
  • u2 name_index : 이름 상수 포인터
  • u2 descriptor_index : 필드 및 메서드 서술자를 참조.
    • 서술자 : 완전한 이름 상수와 달리, 필드의 데이터 타입, 매개변수 목록, 반환값 등까지 기술함.
    • 서술자 식별문자 : p.307 표 6-6 참고
      • void inc() -> V() = void 리턴 메서드
      • int indexOf(char[] source, int index) -> ([CI)I
  • u2 attribute_count : 필드 갯수, 0 이면 값 선언이 되지않은 필드.
  • attribute_info attributes[attribute_count] : 필드 요소.

그외

  1. 상속받은 필드는 필드 테이블에 나열하지 않는다.
  2. innerClass 인 경우, 코드엔 없는 외부 클래스 포인터 필드가 추가된다.
  3. 자바 언어에서는 동일 필드명을 한 클래스안에 선언할 수 없는데,(overloading) 클래스파일 형식에서는 서술자만 다르면 다른 필드로 취급된다.

6. 메서드 테이블

  • 필드테이블과 동일한 구조.
  • 메서드 본문 코드는 attribute_infoCode 속성에 byteCode로 저장된다.
  • 필드와 마찬가지로 이름,매개변수가 같고 반환값이 다른 메서드 오버라이딩이 클래스파일에서는 서술자가 다르므로 허용된다.

7. 속성 테이블(attribute_info)

  • 자체 제작 컴파일러가 새로운 속성 유형을 정의할수있어 자유도가 있다.
  • u2 attribute_name_index : attribute 이름 상수 포인터. 사전 정의된 속성 내에서 정해짐.
    • Code : 자바 코드가 컴파일된 결과인 바이트 코드 명령어들.
  • u4 attribute_length : 속성 컬렉션 길이
  • u1 info[attribute_length] : 속성 값 자체의 구조는 사용자가 정의할 수 있다.

Code 속성

Offset(h)   00  01  02  03  04  05  06  07  08  09  0A  0B  0C  0D  0E  0F  Decoded text
000000C0    --  --  --  --  --  --  --  --  --  --  --  --  --  00  0D  00
000000D0    00  00  1D  00  01  00  01  00  00  00  05  2A  B7  00  02  B1
  • u2 attribute_name_index : attribue 이름 상수 포인터. 여기서는 “Code” 상수 포인터를 가르것
    • 0x00CD 0x00CE= 00 0D = 13
  • u4 attribute_length : 속성의 길이
    • 0x00CF~ 0x00D2 = 00 00 00 1D = 29
  • u2 max_stack : 피연산자 스택의 최대깊이, VM은 이 크기의 스택을 스택프레임에 할당한다.
    • 0x00D3 0x00D4 = 00 01 = 1
  • u2 max_locals : (동시에 존재하는) 지역 변수 테이블에 필요한 저장소 공간(변수 슬롯 갯수). 일반적으로 32bit 데이터타입은 1변수 슬롯, double,long은 2슬롯을 차지.
    • max_stack, max_local은 스택프레임의 메모리 사용량에 직접적인 영향을 주므로, VM은 변수 슬롯을 재사용한다.
    • 지역변수는 생명주기가 짧으므로, ‘동시에 존재하는’ 지역변수들이 차지하는 슬롯의 최대 크기만큼을 max_locals 크기로 잡는다.
    • 0x00D5~0x00D6 = 00 01 = 1
  • u4 code_length : 바이트코드의 길이 , 이론적으로 최댓값은 $2^32$ , 메서드의 길이가 제한을 넘으면 컴파일불가.
    • 0x00D7~0x00DA = 00 00 00 05 = 5
  • u1 code[code_length] : 바이트코드 명령어들이 순서대로 저장되는 바이트스트림, 명령어 각각은 1byte 를 차지하고, 최대 256가지 명령어가 사전 정의되어있다.
    • 0x00DB~0x00DF
      • 2A : aload_0 : 0번째 변수슬롯에 담긴 참조타입 지역변수를 피연산자 스택의 맨 위로 읽어라
      • B7 : invokespecial : 스택 맨위 데이터가 가리키는 객체(위에서 읽은 객체를) 메서드 수신자로 사용하여 생성자나 private method, super.method 를 호출한다. 메서드 정보는 다음에 나오는 매개변수에 담겨있다.
      • 00 01 : invokespecial 의 u2타입 매개변수. 01번째 상수 = <init>() 메서드
      • B1 : return
    • javap -verbose 실행시 Code 에 args_size
      • 인스턴스 메서드 : 최소 1, 실제 매개변수가 없어도 this 를 가르킴.
      • static 메서드 : 최소 0, this가 없음.
  • 예외테이블
      u2 exception_table_length
      {
          u2 start_pc
          u2 end_pc
          u2 handler_pc
          u2 catch_type   // class_info 상수 인덱스
      } exception_table[exception_table_length]
    
    • start_pc ~ end_pc 사이에서 catch_type 발생시 handler_pc줄로 이동하라.
    • catch_type = 0이면 어떤 exception이든 handler_pc로 이동해야한다.
    • 바이트코드에서는 예외처리 목적으로 점프 명령어가있지만, JVM에서는 예외테이블을 사용하라 명시되어있다.

Exceptions 속성

  • 예외테이블과는 전혀 다름. Code 속성과 같은 맥락이다.
  • 메서드가 throw하는 checkedException 을 나열하는 역할.

LineNumberTable 속성

  • 자바 소스코드의 줄번호와 바이트코드의 줄번호 사이의 대응관계.
  • g:none으로 생성여부를 지정할수있다.
    • 부작용 : 에러로깅에 줄번호 안찍힘, 디버깅 중단점 불가
  • Exception 로깅시 찍히는 lineNumber 생성에 많은 CPU자원이 소모된다고함.

LocalVariableTable 과 LocalVariableTypeTable 속성

  • LocalVariableTable : 스택프레임안의 지역변수와 자바소스코드 변수 사이 관계 설명하는 속성.
  • g:none으로 생성여부를 지정할 수 있다.
    • 부작용 : 메서드를 참조할때 매개변수 이름을 알수없다. arg0, arg1 식으로 나옴.

      mybatis 사용시 -parameter 옵션을 넣어서 빌드했던 사례. javac 컴파일 결과에 method parameter 이름이 남지 않아서 reflection에 의지하는 mybatis mapper 등이 제대로 동작하지 않게 된다. (arg0, arg1 등으로 바뀌어버림)

  • LocalVariableTypeTable : JDK5~ Generic 타입에서 사용, LocalVariableTable 과 구조는 동일하며, ‘변수 상수 인덱스’가 “변수 시그니처 인덱스” 필드로 대체됨.

SourceFile 과 SourceDebugExtension 속성

  • class파일을 생성한 java 소스파일 이름이 기록된다.
  • g:none으로 생성여부를 지정할 수 있다.
  • SourceDegugExtension : 컴파일러에 의해 동적으로 생성된 클래스에 개발자를 위한 정보가 추가됨.
    • JDK5~ JSP도 중단점으로 디버깅할 수 있게됨.

ConstantValue 속성

  • static 키워드로 선언된 클래스변수 에만 이 속성이 붙는다. 상수풀 인덱스를 속성으로 가진다.
  • oracle javac 에서 클래스 변수 초기화 방식
    • final static 이고, String 혹은 primitive type 인 경우 : ConstantValue
    • final이 아니거나, 위 타입이 아닌경우 : () 클래스 생성자에서 초기화.

      primitive type인경우 static final 로 저장해야 GC도 타지않고, 상수풀에 저장되어 효율적. DateFormatter같은 일반객체의 상수화 ? static final, static 차이는 불변가변의 차이, 이것도 thread-safe하고 재활용된다면 클래스변수로 선언하는것이 효율적.

InnerClasses 속성

  • 내부 클래스와 호스트 클래스 사이의 연결관계를 기록. 포맷이 outer 기준임.(inner 여러개를 가지고있는 포맷)

Deprecated 와 Synthetic 속성

  • flag타입의 bool 속성이다.
  • Deprecated : @Deprecated 를 붙인 경우
  • Synthetic : 컴파일러가 자동 생성한 필드나 메서드인경우
    • ex) enum values()
  • attribute_name_index 가 지정되면 그 속성이 deprecated 되었다고 알수있는듯?

    deprecated는 컴파일단계에서 필요한가? 주석아니었나

    • 컴파일러나 JVM에서 사용시 경고로그를 띄울 수 있음.
    • Reflection 에서도 사용됨,
    • IntelliJ 에서도 class 파일 메타데이터를 읽어서 경고.
    • java.lang.Deprecated : @Retention(RetentionPolicy.RUNTIME)

StackMapTable 속성

  • VM이 클래스를 로드할때 바이트코드 검증단계에서 타입검증기가 활용한다.
  • ~JDK6 데이터 흐름을 분석하여 타입 추론.
  • JDK7~ 컴파일단계에서 검증타입을 확인하여 클래스 로딩 성능 향상.

Signature 속성

  • Generic을 지원하기위한 속성.
  • java는 컴파일 후에 bytecode(Code 속성)에서 제네릭 정보를 알수없어서 reflection으로 제네릭 정보를 얻을 수 없었는데, Signature가 이 단점을 보완.

BootstrapMethods 속성

  • invokedynamic 명령어가 참조하는 부트스트랩메서드 한정자가 담긴다.
  • java.lang.invoke..invokedynamic 메서드와 밀접. (8.4절 참고)

MethodParameters 속성

  • JDK 초기에는 메서드의 매개변수명은 클래스파일에 기록하지 않았다.
  • LocalVariableTable : Code의 하위 속성으로, 메서드 본문이 없으면 지역테이블도 존재할 수 없는 구조적 한계.
  • MethodParameters : Code속성과 같은 수준으로, JDK8~ 지원. (-parameters)

모듈화 관련 속성

  • JDK9~ module-info.java 가 독립된 클래스 파일로 컴파일되어 저장된다.

런타임 annotation 관련 속성

  • JDK8~ 런타임 어노테이션이 추가됨.
  • 리플렉션 API로 클래스,필드,애너테이션을 가지고올때 이용된다.

Record 속성

  • JDK16~ 불변객체를 의미하며, 이 속성이 있으면 record class

PermittedSubclasses

  • JDK17~ seald class 를 위한 속성, 확장을 허용하는 클래스 목록을 상수풀 인덱스 목록으로 가지고있다.

6.4 바이트 코드 명령어 소개

  • 명령어 대부분이 피연산자 없이 연산코드 하나로 구성되며, 피연산자는 피연산자 스택에 저장된다.
  • 피연산자?
    • 1+1 -> 1 : 피연산자, + 연산자
  • 연산 코드길이가 1byte로 제한되기때문에 최대 256개의 연산코드만 표현할 수 있다.
  • 클래스 파일 구조에서는 컴파일된 코드에 들어있는 피연산자의 길이정렬을 허용하지 않는다.

    피연산자의 길이정렬 : byte가 다른 객체를 저장할때 padding을 넣지않는다.

    • 1byte가 넘으면 특정구조로 재구성한다. ex) (byte1 « 8) byte2 = 2byte를 하나의 정수로 합칠수있음.

6.4.1. 바이트코드와 데이터 타입들

  • 자주쓰이는 연산과 데이터타입 조합에만 전용 명령어를 배정했다. (int,long,flolat,double,char,,) & (load,store,,) => iload, istore,,
  • 전용명령어가 없는 타입은 별도 지시문을 이용해서 지원되는 타입으로 변환해서 사용한다.
  • byte,char,short 전용명령어는 거의 없으며, boolean타입은 하나도 없다. 이들은 int 전용 명령어로 변환되어 실행한다.

6.4.2 load, store 명령어

  • 스택프레임의 “지역변수 테이블”과 “피연산자 스택” 사이에서 데이터를 주고받는데 쓰인다.
  • 지역변수 -> 피연산자 스택 읽기 : load
    • iload_ : 명령어족 : 피연산자가 n으로 고정된 연산자.
      • n : 0이상의 정수, i : int ,,
  • 피연산자 스택 -> 지역변수 저장 : store
  • 상수 -> 피연산자 스택 읽기 : bipush,,const,,ldc

6.4.3 산술 명령어

  • 피연산자 스택의 값 두개로 산술연산을 수행하고, 결과를 다시 피연산자 스택 맨위에 저장.
  • ex) iadd,isub,,,ineg,ior,iand,,dcmpg,dcmpl,,
  • 정수 연산
    • overflow 일때 결과가 명시되어있지않다.
    • 나누는 값이 0이면 ArithmeticException 을 던진다.
    • 그외에 런타임 예외를 던지지않는다.
  • 부동소수점 연산
    • 연산시 수학적 모호성을 없애기 위해, ‘가까운값으로 반올림’ 모드를 사용한다 : 표현가능한 두 값이 ‘수학적으로 정확한’값과 차이가 똑같다면 최하위 비트가 0인 값을 우선한다.
    • 부동소수점 -> 정수 변환시, ‘0에 가까운 값으로 반올림’ 한다. : 정확한 값과 가장 가까우면서 더 크지 않은값을 선택.
    • 부동소수점 연산을 수행할때 런타임 예외를 던지지 않는다.

6.4.4 형 변환 명령어

  • 형변환 명령어는 숫자 타입 데이터를 다른 숫자 타입으로 변환한다.
  • 데이터 타입의 표현범위가 넓어지는 경우에는 변환 명령어를 명시하지 않아도 JVM이 알아서 수행해준다.
    • ex) int -> long, float -> double
  • 데이터 타입을 축소변환하면 오버플로, 언더플로, 정빌도 손실이 발생할수있다. (0에 가까운 값으로 반올림.)
  • RuntimeException을 던지지는 않는다.

6.4.5 객체 생성과 접근 명령어

  • 인스턴스와 배열은 다른 명령어를 사용한다.
  • new, newarray,,,

6.4.6 피연산자 스택 관리 명령어

  • pop, swap, dup,,

6.4.7 제어 전이 명령어

  • PC레지스터의 값을 (무)조건부로 이동
  • ifeq, iflt, ifnull, if_icmplt, tableswitch, goto ,,
  • 모든 타입의 조건분기에서는 모두 (비교연산 명령어 dcmpg, dgmpl,,를 거친 후) int 타입용 조건분기로 마무리한다.
  • JVM에서 int타입용 조건분기 명령어가 가장 풍부하고 강력하다.

메서드 호출과 반환 명령어

  • invokevirtual : 객체의 인스턴스 메서드 호출, 실제 타입에 따라 디스패치.
  • invokeinterface : 인터페이스 메서드 호출, 구현한 객체를 런타임에 검색하여 메서드 찾는다.
  • invokespecial : 인스턴스 초기화 메서드, Private 메서드, super.메서드 등 특수처리가 필요한 일부 인스턴스메서드
  • invokestatic : 클래스 메서드(static) 호출
  • invokesynamic : 런타임에 호출사이트 한정자가 참조하는 메서드를 동적으로 찾아 호출. JVM 실행시 사용자가 설정가능.

6.4.9 예외처리 명령어

  • athrow
  • 예외처리 (catch 문)을 바이트코드 명령어 대신 예외테이블을 이용해 구현한다. (Code 속성의 예외테이블 참고)

6.4.10 동기화 명령어

  • 메서드 수준 동기화 : 상수풀 안의 메서드 테이블에 있는 ACC_SYNCHRONIZED 접근 플래그 확인.
    • 메서드 시작에 모니터(락) 을 획득하고, 정상 완료 여부와 관계없이 종료시 해제.
  • 명령어 블록 동기화 : synchronized {} : monitorenter ~ monitorexit 명령어로 사용.

6.5 설계는 공개, 구현은 비공개

  • ‘자바 가상머신 명세’ 에서 클래스파일 형식과 명령어집합 같은 공통 형식을 정의하고있으며, 각 JVM들은 이 명세는 지키되, 구현은 공개하지 않아도 된다. 인터페이스가 잘 공개되어있기때문에, 자유도높은 고성능 JVM을 구현할수있다.

6.6 클래스 파일 구조의 진화