ClassFile - 1

2024-05-12

entrance

Intro

Java Compiler.java파일을 컴파일하면 ByteCode를 담은 .class 파일이 생성됩니다. .class파일은 Class Loader에 의해 해석되며 JVM 메모리에 로드되어 실행됩니다. 이러한 ByteCode 덕분에 JavaWORA(Write Once, Run Anywhere)라는 강력한 특징을 갖게되었습니다.

javac_bytecode ByteCode에는 또 다른 장점이 있습니다. JVM(자바 가상 머신)은 초기 설계부터 가상 머신에 다른 언어를 실행할 가능성을 염두에 두었습니다. 그래서 자바 외 다른 언어에서 ByteCode로 변환해주는 컴파일러만 있으면 JVM에서 실행될 수 있습니다. 오늘날에는 Java말고도 Scala, Kotlin, Groovy, Clojure 등 다양한 언어가 JVM에서 실행됩니다. 그럼 .class파일에 있는 ByteCode는 무엇이고 어떤 구조로 되어있을까요?

.class File

클래스파일은 바이트를 단위로 하는 이진 스트림 집합체입니다. 각 항목은 정해진 순서에 맞게 구분 기호 없이 나열되어있습니다. 1바이트가 넘는 데이터 항목은 바이트 단위로 분할되며 빅 엔디언 방식으로 표현됩니다. 그래서 클래스파일 전체가 낭비되는 공간없이 프로그램을 실행하는데 필요한 데이터로 채워집니다.

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]; }

바이트스트림은 C 언어의 구조체와 비슷한 구조를 표현하며 부호 없는 숫자테이블 두 가지 타입으로 클래스파일을 나타냅니다. 부호 없는 숫자u1, u2, u4, u8으로 각각 1바이트, 2바이트, 4바이트, 8바이트를 뜻합니다. 테이블타입은 여러 개의 부호 없는 숫자나 또 다른 테이블로 구성된 복합 데이터 타입을 표현합니다. 관례적으로 테이블 타입은 _info으로 표현합니다.

클래스파일의 구조는 클래스를 나타내는 메타정보와 속성으로 이루어져있습니다. attribute_info 테이블이 클래스의 다양한 정보를 담고 있는 속성 테이블입니다. 속성 테이블은 클래스 속성 테이블, 메서드 속성 테이블, 필드 속성 테이블이 있습니다.
클래스 속성 테이블에는 제네릭 매개 변수화 타입, java파일 이름, Inner 클래스 정보가 있습니다.
메서드 속성 테이블에는 해당 메서드의 코드가 저장됩니다.
필드 속성 테이블에는 final 키워드로 정의된 상수값이 저장됩니다.
속성 테이블에 대한 내용도 꽤 많은 내용이 있어 우선 클래스의 메타정보를 나타내는 타입을 살펴보겠습니다.

magic number
모든 클래스 파일의 처음은 4바이트인 매직 넘버로 시작합니다. 매직 넘버는 가상 머신이 허용하는 클래스 파일인지 여부를 확인하는 용도로만 사용됩니다.

minor version
2바이트로 JDK 마이너 버전을 표현합니다.

major version
2바이트로 JDK 메이저 버전을 표현합니다.

constant_pool_count
버전 다음으로는 상수 풀을 나타냅니다. 상수 풀은 고정적이지 않으므로 상수 풀에 앞서 항목의 개수를 알려주는 2바이트 데이터가 필요합니다.

constant_pool[constant_pool_count-1]
상수 풀에는 다양한 문자열 상수, 클래스인터페이스 완전한 이름, 필드 이름서술자, 메서드 이름서술자 등을 나타냅니다. .class파일에서 대체로 가장 큰 부분을 차지합니다. 상수 풀 안의 상수는 모두 테이블 타입입니다. JDK21 기준으로 총 17가지 상수 타입이 존재합니다.

CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }

위 상수 타입은 클래스 내의 메서드를 나타내는 타입입니다.
tag상수 풀에서의 Methodref가 갖는 태그번호이며 u2 class_index메서드가 정의된 클래스 정보, u2 name_and_type_index메서드의 이름타입 정보를 타나내는 인덱스 입니다.

access_flags
상수 풀 다음의 2바이트는 현재 클래스의 접근 정보를 식별하는 접근 플래그입니다. public인지, abstract인지, final인지 등의 여부를 나타냅니다.

this_class, super_class, interface_account
다음으로는 현재 클래스의 인덱스부모 클래스 인덱스, 인터페이스 인덱스 컬렉션입니다. 인덱스상수 풀에 대한 유효한 인덱스로 구성되며 클래스 파일의 상속 관계를 규정합니다.

fields_count
필드 테이블인터페이스클래스 안에 선언된 변수들을 나타냅니다. 필드 테이블 또한 고정된 값이 아니므로 필드 테이블을 나타내기 앞서 개수를 알려주는 2바이트가 필요합니다.

field_info[fields_count-1]
필드에 접근할 수 있는 접근 범위제한자(public, private, protected), 불변 여부,휘발성, 직렬화 시 포함여부, 데이터 타입, 필드 이름의 정보를 담고 있습니다.

field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }

access_flags는 해당 필드가 public, private, protected인지 여부를 표현하는 바이트2 자리로 표현합니다. name_index는 필드의 단순 이름을 나타내며 상수 풀에서 CONSTANT_NameAndType 인덱스를 나타냅니다. descriptor_index필드의 경우 데이터 타입까지, 메서드의 경우 매개 변수 목록(개수, 타입, 순서 포함)과 반환값까지 기술합니다.

methods_count
메서드 테이블의 개수를 나타내는 2바이트입니다.

method_info

method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }

메서드 테이블의 구조도 필드의 테이블구조와 동일합니다. 접근 범위제한자(public, private, protected), 이름 인덱스, 서술자 인덱스입니다. 메서드의 본문(코드)속성 테이블에 저장됩니다.

ByteViewer

javap 커맨드를 통해서 를 통해서 .java 파일의 내용이 .class에서는 어떻게 표현되는지 확인해보겠습니다.

package org.jovialcode.base; public class Bytecode { private static final String field = "string"; public static String staticMethod() { return "staticMethod"; } }

javap -v는 바이트 코드의 순서대로 출력하는 것이 아니라 클래스의 구조를 더 이해하기 쉽게 정리하여 출력합니다. 위 커맨드는 클래스의 구성 요소를 섹션별로 그룹화하고, 각 요소의 세부 정보를 표시합니다.

> javap -v -c .\Bytecode.class Classfile /java/main/org/jovialcode/base/Bytecode.class Last modified 2024. 5. 12.; size 444 bytes MD5 checksum 241941b8981b9e32e706593708b84fcb Compiled from "Bytecode.java" public class org.jovialcode.base.Bytecode minor version: 0 //마이너 버전 major version: 62 //메이저 버전 flags: (0x0021) ACC_PUBLIC, ACC_SUPER // 접근 제어자 this_class: #9 //Bytecode 클래스 인덱스 // org/jovialcode/base/Bytecode super_class: #2 //Bytecode가 상속하는 클래스 // java/lang/Object interfaces: 0, fields: 1, methods: 2, attributes: 1 Constant pool: //상수풀 #1 = Methodref #2.#3 // java/lang/Object."<init>":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "<init>":()V #4 = Utf8 java/lang/Object #5 = Utf8 <init> #6 = Utf8 ()V #7 = String #8 // staticMethod #8 = Utf8 staticMethod #9 = Class #10 // org/jovialcode/base/Bytecode #10 = Utf8 org/jovialcode/base/Bytecode #11 = Utf8 field #12 = Utf8 Ljava/lang/String; #13 = Utf8 ConstantValue #14 = String #15 // string #15 = Utf8 string #16 = Utf8 Code #17 = Utf8 LineNumberTable #18 = Utf8 LocalVariableTable #19 = Utf8 this #20 = Utf8 Lorg/jovialcode/base/Bytecode; #21 = Utf8 ()Ljava/lang/String; #22 = Utf8 SourceFile #23 = Utf8 Bytecode.java { public org.jovialcode.base.Bytecode(); descriptor: ()V //메서드 서술자 flags: (0x0001) ACC_PUBLIC // 메서드 접근제어자 Code: //코드 stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/jovialcode/base/Bytecode; public static java.lang.String staticMethod(); descriptor: ()Ljava/lang/String; flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #7 // String staticMethod 2: areturn LineNumberTable: line 7: 0 } SourceFile: "Bytecode.java"

Reference

오라클 문서
JVM 밑바닥까지 파헤치기