-
JVM 바이트코드 알아보기Java 2025. 1. 4. 02:35
바이트코드
자바는 익히 알려진대로 자바 가상 머신(JVM) 위에서 실행되는 프로그래밍 언어이다.
동작 구조는 다음과 같다.
1. 자바 소스 코드를 작성
2. 컴파일하여 바이트 코드로 변환
3. 바이트 코드를 JVM에 로딩
4. 로드된 바이트 코드를 인터프리터 혹은 JIT 컴파일러를 통해 실행
개발자가 작성하는 자바 코드는 컴파일된 .class 파일, 바이트코드가 되어 JVM에게 전달된다.
JVM이 이해하는 바이트코드는 어떤 구조로 되어있는 지 살펴보며, 간략한 동작 과정들을 이해해보았다.
바이트코드는 특정한 구조를 가진 이진 스트림 형태로 자바 가상 머신 명세에 ClassFile 이라는 구조로 정의되어 있다.
ClassFile
- .java 파일을 자바 컴파일러로 컴파일한 .class 파일의 구조
- JVM(자바 가상 머신)이 이해할 수 있는 바이트코드
- ClassFile의 구조는 "자바 가상 머신 명세" 에 정의되어 있으며, 일정한 형식과 순서를 따른다.
- ClassFile은 이진 스트림 형태이며, 빈틈없이 필요한 데이터가 순서에 맞게 차곡차곡 채워져있다.
- 1 바이트가 넘는 데이터 항목은 바이트 단위로 분할되며, 바이트 저장 순서 방식은 빅 엔디안 방식이다.
- 빅 엔디안 : 낮은 주소에 높은 바이트부터 저장하는 방식이며, 사람이 숫자를 읽는 방식과 똑같이 순서대로 직관적으로 읽을 수 있다.
- 리틀 엔디안 : 낮은 주소에 낮은 바이트부터 저장하는 방식이며, 사람이 숫자를 읽는 방식과 반대로 읽어야한다. CPU는 계산을 할 때 낮은 바이트부터 접근하여 계산을 하기에 산술연산에 적합
- ClassFile은 순서가 중요한 이진 스트림 형태이기에, 빅 엔디안 방식을 선택
- ClassFile 구조는 XML 같은 언어를 이용하지 않기 때문에 구분자가 없다. 따라서 각 바이트의 의미, 길이, 순서가 엄격하게 제한되며 변경할 수 없다.
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]; }
ClassFile 의 데이터 타입
- 부호 없는 숫자
- u1, u2, u4, u8 : 각각 1, 2, 4, 8 바이트를 뜻한다.
- 숫자, 인덱스 참조, 수량 값을 기술하거나 UTF-8로 인코딩된 문자열 값을 구성
- 테이블
- 여러 개의 부호 없는 숫자 또는 또 다른 테이블로 구성된 복합 데이터 타입을 표현
- 구분이 쉽도록 테이블 이름은 관례적으로 "_info"로 끝난다.
ClassFile 눈으로 확인하기
- .java 파일 작성
- 컴파일하여 .class 파일 생성
- 컴파일된 .class 파일을 javap 명령어(java Disassembler)를 사용하여 사람이 보기 쉬운 형태로 요약
- javap -v Main
- -v 설정은 바이트 코드 명령어를 포함하여 자세히 출력
ClassFile 구조 순서
1. 매직 넘버와 클래스 파일 버전
- 클래스 파일은 이진 스트림 형태라고 했다.
- 처음 4바이트는 해당 클래스 파일이 가상 머신이 허용하는 클래스 파일인지 여부를 확인하는 매직넘버이며 값은 0xCAFEBEBE 이다. 데이터 타입은 u4
- 5~6 바이트는 마이너 버전 정보를 나타낸다. u2
- 주로 클래스 파일 형식의 세부 변경 사항을 나타낸다.
- 대부분의 경우, 마이너 버전은 0으로 설정되며, 큰 의미를 가지는 경우는 드물다.
- 7~8 바이트는 메이저 버전 정보를 나타낸다. u2
- .class 파일을 생성한 자바 버전과 직접적으로 매핑
- JVM은 클래스 파일의 메이저 버전을 확인하여 해당 파일을 실행할 수 있는지 확인
2. 상수풀
- 상수풀의 정보는 태그를 통해 타입을 구분한다.
- 상수 풀에 저장되는 타입은 리터럴, 심벌 참조 두가지이다.
- 리터럴 : 자바 언어 수준에서 이야기하는 상수와 비슷한 개념
- 심벌 참조 : 컴파일과 관련된 개념으로, 다음과 같은 정보가 포함된다.
- 모듈에서 익스포트하거나 임포트하는 패키지
- 클래스와 인터페이스의 완전한 이름 (fully qualified name)
- 필드 이름과 서술자
- 메서드 이름과 서술자
- 메서드 핸들과 메서드 타입
- 동적으로 계산되는 호출 사이트와 동적으로 계산되는 상수
- 자바의 컴파일 과정에서는 C, C++ 과 달리 링크 단계가 없다.
- 자바는 컴파일 시 심벌 참조를 기록하고, 실행 시 동적 연결로 이를 해결
- 동적 연결은 클래스 로더가 담당하며, 실행 시 필요할 때 클래스와 심볼을 메모리에 로드하고 연결
- 상수풀에 저장된 테이블은 인덱스를 통해 접근이 가능하다.
컴파일 시점의 인덱스와 메모리 주소의 관계
- 컴파일 시점의 인덱스는 바이트코드에서 데이터를 참조하기 위한 상대적 식별자로 사용
- 컴파일 시점의 인덱스는 실제 메모리 주소와는 직접적인 관계가 없음
- 메모리 주소는 JVM이 클래스를 로드하는 시점에 정해짐
- 따라서, 컴파일 시점의 인덱스는 실제 데이터를 참조하기 위한 마커 역할을 한다고 이해할 수 있음3. 접근 플래그
- 상수풀 다음의 2바이트는 클래스 또는 인터페이스의 접근 정보를 식별하는 접근 플래그
public
,final
,interface
,abstract
, 컴파일러가 자동 생성한 클래스,annotation
,enum
,module
에 대한 여부를 표현한다.- ACC_SUPER 플래그는 JDK 1.0.2 이후 버전이라면 항상 true 로 표시된다.
4. 클래스 인덱스, 부모 클래스 인덱스, 인터페이스 인덱스
- 자바는 다중 상속을 허용하지 않으므로 클래스 인덱스, 부모 클래스 인덱스의 데이터 타입은 단일 u2 타입
- 인터페이스는 다중 상속이 가능하므로 u2 데이터들의 묶음으로 표현
- 모든 자바 클래스는
java.lang.Object
를 상속받기에java.lang.Object
를 제외한 모든 클래스의 부모 클래스 인덱스 값은 0이 아님
5. 필드 테이블
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
- 인터페이스나 클래스 안에 선언된 변수들에 대한 정보
- 접근 제한자,
static
,final
,volatile
,transient
, 데이터 타입, 필드 이름 - 접근 제한자,
static
,final
,volatile
,transient
, 컴파일러가 자동으로 생성한 필드인지 여부,enum
필드인지 여부에 대한 정보는 플래그로 표현 - 필드 이름, 타입에 대한 정보는 상수 풀을 참조하여 표현
- 부모 클래스나 부모 인터페이스로부터 상속받은 필드는 포함 안됨
6. 메서드 테이블
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
- 필드 테이블과 매우 유사하다.
- 하지만 필드 속성에는 존재하는
volatile
,transient
키워드를 메서드에서는 사용할 수 없으니 access_flag 에서 제외되었다. - 대신 필드 속성에는 없는
synchronized
,native
,strictfp
(부동 소수점 연산을 강제하는 키워드), abstract 키워드에 대응하는 flag 속성이 추가되었다. - 코드 본문은 바이트코드 명령어로 컴파일 되어 attributes 에 "Code" 속성으로 저장된다.
7. 속성 테이블
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
- 클래스 파일, 필드 테이블, 메서드 테이블, 레코드 구성 요소는 특정한 정보를 설명하기 위해 고유한 속성 테이블을 포함할 수 있다.
- 클래스 파일은 순서, 길이, 내용이 엄격하지만 속성 테이블은 그에 반해 느슨하며 순서에도 엄격하지 않다.
- JVM이 모르는 속성 정보라면 그냥 무시해버린다.
- 속성 이름은 모두 상수풀의
CONSTANT_utf8
을 참조해 표현하며, 길이는 u4 타입을 표현하며 이 기준만 지킨다면 사용자 정의하여 사용이 가능하다.
다음은 주요 속성이다.
Code_attribute
메서드 테이블인
method_info
에 위치하고 있으며, 메서드의 실행 코드의 내용이 담기는 테이블Code_attribute { u2 attribute_name_index; // 속성 이름을 가리키는 상수의 인덱스. Code 고정 u4 attribute_length; // 속성의 길이. Code 길이 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]; }
아래는 각 필드들에 대한 설명이다.
- max_stack
JVM 스택
운영체제에서 프로세스는 메모리를 독립적으로 사용하지만 스레드는 스택 영역을 제외한 메모리 공간을 공유한다.
즉 스레드마다 고유한 스택 영역이 생긴다.JVM은 스레드를 생성할 때 고유한 스택 영역을 생성하고, 메서드가 호출될 때 프레임을 생성한다.
프레임의 구성
Local Variables(지역 변수 배열)
Operand Stack(피연산자 스택)
Run-time Constant Pool Reference
Operand Stack(피연산자 스택) : JVM 이 연산을 수행할 때 데이터를 임시로 저장하는 공간.public int test() { int a = 32767; int b = 32768; return a + b; }
위의 코드는 파라미터로 a 와 b 를 받아 리턴 값으로 더하는 간단한 함수이다.
바이트코드를 통해 JVM이 어떻게 연산하는 지 알아보자.Code: stack=2, locals=3, args_size=1 0: sipush 32767 // 16비트의 크기까지는 sipush. 상수풀 참조 X.PUSH 3: istore_1 // variable 1 에 저장. POP 4: ldc #14 // int 32768. 상수풀 참조. PUSH 6: istore_2 // variable 2 에 저장. POP 7: iload_1 // variable 1을 PUSH 8: iload_2 // variable 2을 PUSH 9: iadd // STACK 의 두개를 POP하여 더하기 후 PUSH 10: ireturn // STACK 을 POP하여 RETURN
간단한 그림으로 옮기면 위와 같은 과정을 거치게 된다. 보다시피 Operand Stack의 크기가 2를 넘지 않는다.컴파일 과정에서 Operand Stack의 최대 max_stack의 크기를 계산하고 런타임에 스레드의 스택 프레임에 이를 할당하고, 스레드는 절대 max_stack 크기를 초과하여 Operand Stack을 사용할 수 없다.
간단한 코드를 컴파일하며 바이트코드를 확인한 결과
- 1 byte 까지는 bipush 명령어로 값을 입력한다. -128 ~ 127
- 2 byte 까지는 sipush 명령어로 값을 입력한다. -32768 ~ 32767
- 값의 범위가 2 byte 를 초과한다면 값을 상수풀에 저장한 뒤, ldc 명령어로 상수풀에서 참조한다.
- max_locals
지역 변수 테이블에 필요한 슬롯의 크기를 뜻한다.
4 byte 를 넘지 않는 데이터 타입은 1개의 슬롯을 차지 하고, (byte, short, char, int, boolean, float)
4 byte 를 초과하는 데이터 타입은 2개의 슬롯을 차지한다.(double, long)변수에는 메서드 파라미터(인스턴스 메서드의 경우 숨겨진 this 매개 변수 포함), 명시적 예외 핸들러 매개 변수(try-catch 문의 catch 블록에 정의된 예외), 메서드 본문에 정의된 지역 변수가 포함된다.
변수 슬롯을 불필요하게 많이 사용하는 것은 메모리 사용량에 영향을 주므로, JVM은 사용을 마친 변수 슬롯을 재사용하며 최대한 효율적으로 사용한다. 따라서 max_locals 는 모든 선언된 변수의 갯수라기보단 효율적으로 사용할 때 동시에 선언되는 최대 갯수의 의미에 가깝다.
- code_length
소스 코드가 컴파일된 바이트 코드의 길이이다. 데이터 타입은 u4로 이론상 2^32 까지 가능하지만 자바 가상 머신 명세에는 65536까지 밖에 되지 않는다고 적혀있다.
The value of code_length must be greater than zero (as the code array must not be empty) and less than 65536.
출처 : https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.3
따라서 사실상 u2의 타입이라고 봐야한다.- code
바이트코드 명령어가 순서대로 저장되는 바이트 스트림이다. 바이트코드 명령어라는 이름처럼 명령어는 u1 타입의 1바이트 이다.
1바이트는 최대 256의 값을 가질 수 있고, 현재 지원하는 JVM 바이트코드 명령어는 약 200개 정도 된다고 한다. Java bytecode위 max_stack 설명의 바이트코드에 나오는
sipush
,ldc
,iload1
등등이 바이트코드 명령어이다.- exception
start_pc : 시작 명령어 주소
end_pc : 종료 명령어 주소
handler_pc : 예외 발생시 돌아갈 주소
catch_type : 검사할 예외 혹은 타입이 0이라면 handler_pc 로 돌아감public int inc() { int i; try { i = 1; return i; } catch (Exception e) { i = 2; return i; } finally { i = 3; } } Exception table: from to target type 0 4 8 Class java/lang/Exception // 0 ~ 4 에서 type 에러가 발생하면 8 0 4 17 any // 0 ~ 4 에서 type 이외의 모든 예외가 발생하면 17 8 13 17 any // 8 ~ 13 에서 type 이외의 모든 예외가 발생하면 17 17 19 17 any // 17 ~ 19 에서 type 이외의 모든 예외가 발생하면 17
위의 간단한 코드를 컴파일 하면 모든 예외 상황에 대한 테이블을 생성하는 것을 확인할 수 있다.
Exceptions_attribute
Exceptions_attribute { u2 attribute_name_index; // 상수풀의 Exceptions 참조 u4 attribute_length; u2 number_of_exceptions; // 메서드가 던질 수 있는 예외 개수 u2 exception_index_table[number_of_exceptions]; // 상수풀에서 예외 참조 }
- 예외에 대한 속성 테이블로 메서드 테이블에 속하는 정보이다.
public int inc() throws IOException, InterruptedException { return 1; } Exceptions: throws java.io.IOException, java.lang.InterruptedException
- 위와 같은 메서드에서 throws 는 해당 메서드가 던질 수 있는 예외를 정의하는 것이다.
- 그리고 이 예외들이
Exceptions_attribute
에 속한다.
LineNumberTable_attribute
LineNumberTable_attribute { u2 attribute_name_index; // 상수풀의 LineNumberTable 참조 u4 attribute_length; u2 line_number_table_length; { u2 start_pc; // 바이트코드의 줄 번호 u2 line_number; // 자바 소스 코드의 줄 번호 } line_number_table[line_number_table_length]; }
- 자바 소스 코드의 줄 번호와 바이트코드의 줄 번호 사이의 대응 관계를 설명하는 속성으로,
Code_attribute
내부의 attribute에 속하는 정보이다. - 필수 속성이 아니며 javac 에 -g:none, -g:lines 옵션으로 생성되지 않게 할 수 있다.
- 이 속성이 없다면 예외 stack trace 시 줄 번호에 대한 정보가 나타나지 않고, 디버깅할 때에도 자바 소스의 특정 줄에 브레이크 포인트를 설정할 수 없다.
LocalVariableTable_attribute, LocalVariableTypeTable_attribute
LocalVariableTable_attribute { u2 attribute_name_index; // 상수풀의 LocalVariableTable 참조 u4 attribute_length; u2 local_variable_table_length; { u2 start_pc; // 지역 변수의 유효 범위가 시작되는 바이트코드 오프셋 u2 length; // 유효 범위의 길이 u2 name_index; // 지역 변수 이름. 상수풀 참조 u2 descriptor_index; // 지역 변수 서술자. 상수풀 참조 u2 index; // 스택 프레임의 Local Variables에서 슬롯 테이블 } local_variable_table[local_variable_table_length]; } // 제네릭 도입으로 추가된 속성 LocalVariableTypeTable_attribute { u2 attribute_name_index; // 상수풀의 LocalVariableTypeTable 참조 u4 attribute_length; u2 local_variable_type_table_length; { u2 start_pc; u2 length; u2 name_index; u2 signature_index; // 지역 변수 '시그니처' 를 가리키는 상수의 인덱스 u2 index; } local_variable_type_table[local_variable_type_table_length]; }
- 스택 프레임 내에 있는 Local Variables 의 변수와 자바 소스 코드에 정의된 변수 사이의 대응 관계를 설명하는 속성으로
Code_attribute
내부의 attribute에 속하는 정보이다. - 필수 속성이 아니며 javac 에 -g:none, -g:vars 옵션으로 생성되지 않게 할 수 있다.
- 이 속성이 없다면 디버깅 모드에서 매개 변수 이름을 사용해서 값을 가져올 수 없다. 또한 통합 환경에서 해당 클래스를 참조하는 곳에서 임의의 이름으로 변수를 사용해서 불편함이 있다고 한다.
SourceFile_attribute
SourceFile_attribute { u2 attribute_name_index; // 상수풀의 SourceFile 참조 u4 attribute_length; u2 sourcefile_index; // 상수풀의 클래스 이름 참조 }
- 클래스 파일을 생성한 자바 소스 파일 이름에 대한 속성으로,
ClassFile
의 attribute 에 속한다. - 필수 속성이 아니며 javac 에 g:none, g:source 옵션으로 생성되지 않게 할 수 있다.
- 이 속성이 없다면 예외 stack trace 시 파일에 대한 정보가 나타나지 않는다.
ConstantValue_attribute
ConstantValue_attribute { u2 attribute_name_index; // 상수풀의 ConstantValue 참조 u4 attribute_length; u2 constantvalue_index; // 상수풀 참조 }
- 정적 변수에 값을 자동으로 할당하도록 JVM에 알리기 위한 속성으로,
filed_info
의 attribute 에 속하는 정보이다.
InnerClasses_attribute
InnerClasses_attribute { u2 attribute_name_index; // 상수풀의 InnerClasses 참조 u4 attribute_length; u2 number_of_classes; // 내부 클래스 정보의 개수 { u2 inner_class_info_index; // 내부 클래스의 심벌 참조(상수풀 인덱스) u2 outer_class_info_index; // 외부 클래스의 심벌 참조(상수풀 인덱스) u2 inner_name_index; // 내부 클래스 이름(상수풀 인덱스) u2 inner_class_access_flags; // 내부 클래스의 접근 플래그 } classes[number_of_classes]; }
- 내부 클래스와 외부 클래스 사이의 관계에 대한 속성으로,
ClassFile
의 attribute에 속하는 정보이다. - 한 클래스 내에 정의된 내부 클래스에 대한 정보들을 담고 있다.
Deprecated_attribute
Deprecated_attribute { u2 attribute_name_index; // 상수풀 Deprecated 참조 u4 attribute_length; }
- @Deprecated 어노테이션을 달아서 설정할 수 있는 속성으로
ClassFile
,field_info
,method_info
에 속할 수 있는 정보이다. - 플래그 타입의 값으로 length 는 항상 0으로 고정이다.
Synthetic_attribute
Synthetic_attribute { u2 attribute_name_index; // 상수풀 Synthetic 참조 u4 attribute_length; }
- 컴파일러가 추가한 필드, 메서드임을 나타내는 속성으로
ClassFile
,field_info
,method_info
에 속할 수 있는 정보이다. - 플래그 타입의 값으로 length 는 항상 0으로 고정이다.
StackMapTable_attribute
StackMapTable_attribute { u2 attribute_name_index; // 상수풀 StackMapTable 참조 u4 attribute_length; // StackMapTable 전체 크기 u2 number_of_entries; // StackMapFrame 의 개수 stack_map_frame entries[number_of_entries]; // 각 Frame 정보 }
- 바이트코드의 타입 안정성을 보장하기 위해 스택과 로컬 변수의 상태를 기록한 속성.
- 런타임에 JVM이 바이트코드 검증을 수행할 때, 상태를 효율적으로 추적하도록 돕는다.
Code attribute
내부에 포함되며, 메서드의 바이트코드 상태를 나타냄- stack_map_frame:
- 각 프레임은 특정 바이트코드 오프셋에서의 로컬 변수와 스택의 타입 상태를 나타낸다.
- 프레임은 다음과 같은 유형으로 구성:
- same_frame : 상태가 이전 프레임과 동일.
- same_locals_1_stack_item_frame : 로컬 변수는 동일하고, 스택에 한 항목이 추가된 상태.
- chop_frame : 로컬 변수가 제거된 상태.
- append_frame : 로컬 변수가 추가된 상태.
- full_frame : 로컬 변수와 스택 상태가 완전히 기술된 상태.
Signature_attribute
Signature_attribute { u2 attribute_name_index; // 상수풀 Signature 참조 u4 attribute_length; u2 signature_index; }
- 제네릭 시그니처에 대한 정보를 나타내는 속성으로,
ClassFile
,field_info
,method_info
에 속할 수 있는 정보이다. - 자바는 제네릭을 소거법으로 구현하였다. 컴파일 시점에 타입을 추론하기 때문에, 컴파일된 바이트 코드에는 제네릭 정보가 남아있지 않다.
- 따라서 리플렉션을 사용해 런타임에 제네릭 정보를 가져오려고 해도 바이트 코드에는 제네릭 정보가 남아있지 않기에 사용이 불가능하다.
- 이러한 단점을 해결하기 위해 추가된 속성이
Signature_attribute
이다. 구현 자체는 그대로이지만 바이트 코드 내에 제네릭 정보를 남겨 런타임시에 접근이 가능하다.
BootstrapMethod_attribute
BootstrapMethods_attribute { u2 attribute_name_index; // 상수풀 BootstrapMethod 참조 u4 attribute_length; u2 num_bootstrap_methods; // 부트스트랩 메서드 개수 { u2 bootstrap_method_ref; // 부트스트랩 메서드 참조(상수풀 인덱스) u2 num_bootstrap_arguments; // 부트스트랩 메서드가 받는 정적 인수 개수 u2 bootstrap_arguments[num_bootstrap_arguments]; // 정적 인수 배열 } bootstrap_methods[num_bootstrap_methods]; // 부트스트랩 메서드 배열 }
- invokedynamic 명령어의 실행 중 필요한 정보를 제공하는 속성으로
ClassFile
의 attribute에 속하는 정보이다. - invokedynamic 명령어는 동적 메서드를 호출하는 바이트코드 명령어이다.
- 이와 관련된 내용은 람다의 바이트코드를 분석해보며 깊이 있게 알아볼 예정이다.
이 밖에도
Annotation
,Record
,Selead class
등 관련된 다양한 속성이 존재한다.
참고 : https://product.kyobobook.co.kr/detail/S000213057051
참고 : https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.23참고 : https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions
'Java' 카테고리의 다른 글
JVM 클래스로더 알아보기 (0) 2025.01.11 JVM 클래스 로딩 과정 알아보기 (0) 2025.01.09 자바와 코틀린 (0) 2024.08.29 커스텀 어노테이션 (0) 2024.02.13 OpenAPI 데이터베이스 저장 (0) 2023.05.02