Java 깊이 살펴보기 02: Class
바이트코드 한 땀 한 땀 살펴보기
우리는 Java 깊이 살펴보기 01: Class에서 SimpleClass
를 컴파일하고 바이트코드의 일부를 보았습니다. 오늘은 이어서 남은 바이트코드의 내용을 살펴보고자 합니다.
오늘 글의 주요 목적은 자바의 클래스가 어떻게 바이트 코드로 표현되는지 이해하는 것입니다. 예제 코드는 지난 시간과 동일하게 다음의 SimpleClass.java
입니다.
public class SimpleClass {
public static void main(String[] args) {
System.out.println("Deep dive into Java");
int myData = 0x12345678;
System.out.println("My Data: " + myData);
}
}
SimpleClass.java
를 컴파일한 결과물인 SimpleClass.class
의 바이트코드를 (개인적으로 처음이자 마지막으로 지루하지만) 하나씩 하나씩 꼼꼼히 살펴봅시다.
ClassFile 구조
먼저 다시 한 번 지난 글에서 살펴본 클래스 포맷을 보겠습니다. JVM 명세는 다음과 같이 ClassFile 구조를 정의하고 있습니다.
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];
}
위 구조에서 u4, u2의 의미는 unsigned 바이트 수를 말합니다. SimpleClass.class
의 바이트코드 앞 부분을 다음과 같았습니다.
0000000 ca fe ba be 00 00 00 34 00 2e 0a 00 0d 00 16 09
0000010 00 17 00 18 08 00 19 0a 00 1a 00 1b 03 12 34 56
0000020 78 07 00 1c 0a 00 06 00 16 08 00 1d 0a 00 06 00
0000030 1e 0a 00 06 00 1f 0a 00 06 00 20 07 00 21 07 00
...
이 바이트코드에서 우리는 최초 8바이트 내용까지 다뤘습니다. 한번 더 복습하면 바이트코드의 최초 8바이트는 다음과 같은 의미입니다.
- CA FE BA BE: 4바이트 지정값
- 00 00: 자바의 마이너 버전 명시
- 00 34: 자바의 메이저 버전 명시
메이저 버전 뒤에 두 바이트 00 2E
를 명세는 Constant Pool의 수로 정의하고 있습니다. 그렇다면 Constant Pool은 뭘까요?
Constant Pool
Constant Pool은 Class에서 사용하는 모든 심볼 정보를 담고 있는 표입니다. Constant Pool에는 여러분이 작성한 Java 코드의 변수, 문자열 리터럴 등을 포함하여 컴파일러가 변환한 코드에 대한 모든 내용을 담고 있습니다. 이해하기 쉽게 클래스에서 사용하는 모든 정보를 하나의 배열에 담아 넣었다고 보면 됩니다.
지난 시간에 잠깐 사용했던 javap
의 출력 결과를 보면 Constant Pool이 무엇인지 한눈에 파악할 수 있습니다. javap
의 결과 중 Constant Pool 부분만 보면 다음과 같습니다.
Constant pool:
#1 = Methodref #13.#22 // java/lang/Object."":()V
#2 = Fieldref #23.#24 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #25 // Deep dive into Java
#4 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Integer 305419896
#6 = Class #28 // java/lang/StringBuilder
#7 = Methodref #6.#22 // java/lang/StringBuilder."":()V
#8 = String #29 // My Data:
...
출력된 결과를 보면 #1 부터 시작하여 Constant Pool에 Constant 정보가 나열되어 있음을 알 수 있습니다. 전체 Constant의 수는 바이트코드에 명시된 Constant Pool의 수에서 1을 뺀 값입니다. 그 이유는 Constant Pool의 인덱스가 1부터 시작하기 때문이라고 명세에서 설명하고 있습니다. 물론 위에 ClassFile의 구조에서도 constant_pool배열의 인덱스에 -1
이 붙은 것을 통해서도 알 수 있습니다.
이제 바이트코드의 값을 보면 9-10번째 바이트 00 2E
부분이 바로 SimpleClass.class
의 Constant Pool 수를 의미합니다. 이 값은 10진수로 바꿔서 SimpleClass.class
의 Constant Pool 요소가 45개라는 것을 알려줍니다.
cp_info
다음으로 실제 Pool의 수만큼 Constant 정보가 존재합니다. ClassFile 구조에서는 이 정보를 cp_info
타입으로 표현합니다. cp_info
는 타입에 따라 구조가 다르지만 공통적으로 다음과 같은 구조를 갖습니다.
cp_info {
u1 tag;
u1 info[];
}
첫 번째 바이트인 tag
값에 따라 info
의 구조가 다르다고 볼 수 있습니다. cp_info
의 타입은 명세에 자세하게 정의되어 있습니다. 간략하게 몇가지 타입과 타입에 해당하는 태그 값을 살펴보면 다음과 같습니다.
- CONSTANT_Class | 7
- CONSTANT_Fieldref | 9
- CONSTANT_Methodref | 10
- CONSTANT_InterfaceMethodref | 11
- ...
Constant Pool의 바이트코드를 읽는 것은 정리하면 다음과 같습니다.
- 먼저 9-10번째 2바이트 값에서 Constant Pool의 수를 읽습니다.
- 이후 Constant Pool의 수 만큼
cp_info
구조의 정보를 읽습니다.- 먼저 하나의
cp_info
는 첫번째 바이트에서 타입 정보를 읽습니다. - 각
cp_info
의 구체적인 타입 정보에 따라 이어지는 바이트 정보를 읽습니다.
- 먼저 하나의
뭔가 과정이 복잡하지만 한 땀 한 땀 명세에 있는 내용을 바이트코드와 직접 대조해 보면 결국 위에서 javap
로 출력한 Constant 정보라는 것을 알 수 있습니다.
간단히 몇가지 Constant만 직접 살펴보겠습니다. 예를 들면 첫 번째 Constant를 나타내는 바이트 코드는 11-15바이트인 0A 00 0D 00 16
부분 입니다. 제일 첫 바이트 0A
는 cp_info
의 태그로 CONSTANT_Methodref
를 의미합니다. CONSTANT_Methodref
는 각각 2바이트씩 클래스와 메서드명을 참조하는 Constant Pool의 인덱스를 갖는다고 명세에서 정의합니다. 이에 따라 00 0D
는 클래스의 인덱스로 13이고, 00 16
은 메서드의 인덱스로 22입니다.
정리하면 첫 번째 Constant는 메서드를 참조하는 정보로 #13 인덱스의 클래스의 #22 번째 메서드를 의미합니다. 이는 javap
가 해석한 아래 결과와 동일한 내용이기도 합니다.
#1 = Methodref #13.#22 // java/lang/Object."":()V
그럼 이번에는 #1에서 참조한 #13번 째 인덱스로 한번 건너뛰어 보겠습니다. #2부터 #12까지 명세와 대조해서 바이트들을 지나치고 나면 #13 번째 인덱스에 해당하는 SimpleClass의 바이트 코드 07 00 22
가 나옵니다. 첫번째 바이트 07
은 이 Constant가 Constant_Class
임을 알려줍니다.
Constant_Classs
는 이어지는 2바이트를 Class의 이름을 참조하는 인덱스로 정의합니다. 따라서 00 22
는 실제 #34를 의미하고 이 클래스의 이름이 Constant Pool에 #34째 인덱스에 있다는 의미입니다. 마찬가지로 javap
가 해석한 다음의 결과도 동일한 의미입니다.
13 = Class #34 // java/lang/Object
그럼 이제 또 중간에 바이트들을 훌쩍 뛰어 넘어서 #34에 해당하는 바이트 코드를 찾아보겠습니다. #34에 해당하는 바이트코드는 다음과 같습니다.
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
첫 번째 바이트 01
은 Constant_UTF8
입니다. 이 태그는 UTF-8로 이뤄진 문자를 정의합니다. 명세에 따르면 이어지는 2바이트에서 문자열의 길이를 선언하고, 이어서 그 길이만큼의 문자열 정보를 표현합니다. 00 10
이 16이므로 이어지는 16바이트 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
가 바로 문자열입니다. 결론적으로 이 값이 // java/lang/Object
클래스를 의미합니다.
SimpleClass
에서 사용하는 Constant 정보 총 45개 였습니다. 전체 바이트코드가 (혹시라도) 궁금한 분들은 이 글 맨 아래에 제가 Constant 별로 구분해 놓은 정보를 참조하시면 됩니다. (한번쯤은 일일이 대조해서 확인해 볼만합니다.)
Access Flags
Constant Pool
다음으로 2바이트가 Access Flags 를 표현하는데 할당되어 있습니다. Access Flags는 public
, final
등 클래스나 메서드 앞에 붙는 키워드를 bitwise 조합으로 표현한 값입니다. 예를 들면 public
은 0000000000000001
이고 final
은 0000000000000010
으로 명세에 선언되어 있습니다. 이러한 비트 열 flag를 조합하여 클래스와 메서드에 추가적인 정보를 선언할 수 있습니다.
실제 이 클래스는 00 21
로 선언되어 있는데 이는 ACC_PUBLIC
과 ACC_SUPER
의 조합입니다. 이 정보는 javap
로 해석한 결과에서 public class SimpleClass
정보 밑에서도 동일하게 확인할 수 있습니다.
...
public class SimpleClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
...
Class 정보
다음 정보는 현재 바이트코드의 클래스명과 부모 클래스명입니다. 각각 2바이트로 Constant Pool의 인덱스를 통해서 클래스명을 참조합니다. SimpleClass
의 현재 클래스와 부모 클래스 바이트 코드는 00 0C 00 0D
로 Constant Pool의 #12, #13번에 클래스명이 있습니다.
Interface, Field, Method 정보
지금까지는 바이트코드가 클래스의 기본정보 및 메타데이터만을 표현했습니다. 남은 바이트코드는 실제 우리가 작성한 코드에 대한 상세한 내용이 이어집니다.
Interface 정보
첫번째로 Interface에 대한 정보입니다. Class정보 다음의 두 바이트는 클래스에서 구현한 인턴페이스의 수를 의미합니다. 만약 한 개 이상의 인터페이스를 구현한 경우 이어서 2바이트로 이뤄진 Constant Pool의 인덱스가 있습니다. 이 인덱스들이 실제 클래스에서 정의한 인터페이스의 명칭을 참조하는 것입니다.
SimpleClass
의 예제에서는 구현한 인터페이스가 없기 때문에 Class 정보 다음에 00 00
으로 바이트코드가 따라 붙습니다. 만약 1개의 인터페이스가 있고 인덱스 #4에 Serializable
인터페이스를 구현했다면 이는 바이트코드로 00 01 00 04
와 같이 표현됩니다.
Field 정보
Interface에 대한 정보에 이어서 Class의 field정보가 있습니다. 만약 Class의 field가 선언되어 있다면 Interface와 유사하게 2바이트로 선언된 field의 수만큼 바이트코드가 존재하게 됩니다. SimpleClass
의 경우 선언한 field가 없으므로 Interface와 마찬가지로 00 00
2바이트만 존재하게 됩니다.
만약 1개 이상의 field를 선언했다면 field의 수를 표현하는 2바이트 뒤에 다음과 같은 구조의 field_info
정보가 바이트코드로 존재하게 됩니다.
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Method 정보
이제 바이트코드에서 제일 중요한 부분이 드디어 나왔습니다. field 정보 뒤에는 메서드의 정보가 있습니다. 우리가 컴파일한 자바 클래스를 바이트코드라고 부르는 것과 연관이 있는 부분이 바로 지금부터 설명하는 부분입니다. 메서드 정보에는 먼저 앞선 interface, field와 동일하게 2바이트로 메서드의 수를 표현합니다. 한 개 이상의 메서드가 선언된 경우 바이트코드는 다음과 같은 구조로 메서드 정보를 나타냅니다.
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
제일 먼저 2바이트에 할당된 access_flags
는 public
, final
처럼 앞서 클래스에서 사용한 access_flag
와 같은 내용입니다. 다만 그 대상이 여기서는 메서드일 뿐입니다. 이어지는 2바이트는 메서드의 이름 정보를 참조하는 Constant Pool의 인덱스로 나타냅니다.
Descriptor
이어서 2바이트는 메서드의 Descriptor를 참조하는 Constant Pool의 인덱스 입니다. Descriptor
는 자바의 바이트코드에서 사용하는 메서드 혹은 타입의 고유한 표현방식입니다.
예를 들면 Integer는 Descriptor로 I
로 표현하고 배열은 [
로 표현합니다. 따라서 만약 int[][]
와 같은 정보를 Descriptor로 표현한다면 [[I
로 표기하는 것이 명세에서 정의하는 내용입니다.
메서드로 한가지 예를 더 들어보면 반환값이 void
인 getResult(Long id, String keyword)
와 같은 메서드의 시그니쳐가 있다면 이 메서드는 Descriptor로 다음과 같이 표현합니다.
(Ljava/lang/String;)V;
한번 javap
의 출력결과에서 Constant Pool에 선언된 몇몇 Constant를 다시 봅시다. 위와 같이 Descriptor로 선언된 Constant를 볼 수 있습니다.
attribute_info
2바이트로 이루어진 Descriptor의 정보 다음은 attribute_info가 나옵니다. 여기서도 먼저 2바이트를 할애하여 attribute_info의 수를 나타냅니다. 그리고 다음과 같은 구조로 attribute_info의 수만큼의 정보가 있습니다.
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
attribute_info는 클래스의 여러 정보를 표현할 때 사용됩니다. 명세를 보면 정말 다양한 정보들을 attribute_info 구조를 활용하여 바이트코드로 표현함을 알 수 있습니다. 이 중에서 가장 핵심이 되는 것은 Code attribute 입니다. 이는 실제 메서드의 로직을 담고 있는 속성입니다. 먼저 명세가 정의하고 있는 Code attribute의 구조를 보겠습니다.
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
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];
}
제법 많은 내용들이 있습니다. Code attribute의 구조는 JVM의 동작 원리와 연관이 깊은 내용들이기도 합니다. 따라서 이 부분에 대한 상세한 설명은 다음 글에서 다루기로 합니다.
실제 SimpleClass
에는 2개의 메서드가 있습니다. 하나는 제가 명시적으로 작성한 main
메서드이고 두번째는 JVM에서 객체를 초기화할때 사용하는 <init>
메서드 입니다. <init>
메서드는 특수 메서드로 JVM에서 선언하여 생성하는 메서드로 여러분이 직접 작성할 수 없는 메서드 입니다. 자세한 내용은 Instance Initialization Method에 기술되어 있습니다.
2개의 메서드는 각각 바이트 코드로 Code attribute를 갖고 있습니다. 실제 SimpleClass
의 바이트코드를 살펴보는 것은 내용이 적지 않습니다. 전체 바이트코드의 내용은 글 아래에 참고로 두고 <init>
메서드의 바이트코드를 javap
가 해석한 결과만 봅시다.
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
위에서 보시는 바와 같이 Code:
로 시작하는 부분에 메서드의 메타정보로 stack, locals, args_size라는 것이 선언되어 있는 것을 알 수 있습니다. 이어지는 코드가 바로 Java에서 정의한 opcode
입니다. opcode
는 한 바이트 단위 명령어 집합이므로 256 가지의 명령어를 가질수 있습니다. 우리가 컴파일한 결과를 바이트코드를 부르는 이유도 여기에서 유래한 것으로 알고 있습니다. 다음 글에서 JVM의 동작 원리를 다룰때 opcode
부분도 함께 살펴보기로 합니다.
attribute_info
자 이제 클래스구조의 마지막입니다. 클래스 자체의 속성을 나타내기 위해서 attribute_info를 한번 더 사용합니다. 여기서도 마찬가지로 2바이트로 attribute_info의 수를 표현하고 이어서 attribute_info 구조에 맞게 바이트코드를 해석하면 됩니다.
SimpleClass
의 마지막 10바이트 부분이 이 클래스의 attribute_info 정보입니다. 처음 두 바이트가 00 01
로 시작하기 때문에 attribute가 하나 임을 알 수 있습니다. 이어지는 2바이트가 00 14
를 나타내는데 이는 명세의 정의상 attribute의 이름을 나타내는 Constant Pool 의 인덱스 입니다. #20번째 인덱스를 따라가면 Source File
이라는 것을 알 수 있습니다. Source File attribute는 4바이트로 고정값 00 00 00 02
를 나타내고 남은 2바이트는 바이트코드의 원 소스파일의 이름을 가리키는 Constant Pool의 인덱스를 의미합니다. 마지막 2바이트가 00 15
이고 이에 따라 #21인덱스로 Constant Pool을 찾아보면 SimpleClass.java
라는 것을 알 수 있습니다.
마치며...
개인적으로 처음이자 마지막으로 자바의 컴파일 결과물을 한 땀 한 땀 비교해 보는 작업을 했습니다. 글을 한 번 쭈욱 훑어보시면서 간단한 자바 클래스 하나를 컴파일하여 바이트코드를 javap
명령을 사용하여 살펴보면 좋을 것 같습니다. 물론 시간이 나실때 한 번 정도는 한 땀 한 따 바이트코드를 명세에 정의된 구조를 찾아가면서 이해하는 것도 JVM을 이해하는데 큰 도움이 될 것 같습니다.
한 번 바이트코드의 구조를 이해하고 나니, 머릿 속에 직접 컴파일하지 않고 네트워크나 코드 레벨에서 동적으로 바이트코드를 생성하여 클래스를 만들수 있겠다는 생각이 듭니다. 아마도 이러한 방식이 Bytecode Instrumentation 과 연관된 것으로 알고 있습니다. 이 주제 또한 이 연재에서 다룰 수 있다면 공부하고 함께 다뤄보도록 하겠습니다.
바이트코드 상세정보
다음은 위에서 설명한 바이트코드를 SimpleClass
를 직접 분석하여 의미 단위로 쪼개놓은 내용입니다.
SimpleClass의 Constant Pool 바이트 리스트
- 0A 00 0D 00 16 <= 0A ref 5바이트
- 09 00 17 00 18 <= 09 ref 5바이트
- 08 00 19 <=08 3바이트
- 0A 00 1A 00 1B <= 0A ref 5바이트
- 03 12 34 56 78 <= 03 Int 4바이트 04 float 5바이트
- 07 00 1C <= 07 Class 3바이트
- 0A 00 06 00 16 <= 0A ref 5바이트
- 08 00 1D <= 08 String 3바이트
- 0A 00 06 00 1E
- 0A 00 06 00 1F
- 0A 00 06 00 20
- 07 00 21
- 07 00 22
- 01 00 06 3C 69 6E 69 74 3E <= 01 UTF-8 가변 1 + 2 + (가변)
- 01 00 03 28 29 56
- 01 00 04 43 6F 64 65
- 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
- 01 00 04 6D 61 69 6E
- 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
- 01 00 0A 53 6F 75 72 63 65 46 69 6C 65
- 01 00 10 53 69 6D 70 6C 65 43 6C 61 73 73 2E 6A 61 76 61
- 0C 00 0E 00 0F <=12 0C NameAndTYpe 5 바이트
- 07 00 23
- 0C 00 24 00 25
- 01 00 13 44 65 65 70 20 64 69 76 65 20 69 6E 74 6F 20 4A 61 76 61
- 07 00 26
- 0C 00 27 00 28
- 01 00 17 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 42 75 69 6C 64 65 72
- 01 00 09 4D 79 20 44 61 74 61 3A 20
- 0C 00 29 00 2A
- 0C 00 29 00 2B
- 0C 00 2C 00 2D
- 01 00 0B 53 69 6D 70 6C 65 43 6C 61 73 73
- 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
- 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D
- 01 00 03 6F 75 74 out
- 01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B
- 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
- 01 00 07 70 72 69 6E 74 6C 6E
- 01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
- 01 00 06 61 70 70 65 6E 64
- 01 00 2D 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 42 75 69 6C 64 65 72 3B
- 01 00 1C 28 49 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 42 75 69 6C 64 65 72 3B
- 01 00 08 74 6F 53 74 72 69 6E 67
- 01 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B
Method 정보
첫번째 method_info
access_flags
00 01
name index
00 0E
descriptor index ?
00 0F
attr count
00 01
attr_name_idex 00 10
attr_len 00 00 00 1D (29 바이트)
00 01 <= max stack
00 01 <= max lock
00 00 00 05 <= code length
<=codes (https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.html#jvms-6.5)
2A aload_0
B7 invokespecial
00 01 (index)
B1 return
00 00 <= exception table length
00 01 <= attr count
00 11 <= 다시 attribute 17번 ref Line Number Table
00 00 00 06 <= attr_len
00 01 <= line_number_table-Len
00 00 start_pc
00 01 line_number
두번째 method_info
access_flag
00 09 (00 08 + 00 01)
name index
00 12
descriptor
00 13
attr count
00 01
Attr name index
00 10
Attr length
00 00 00 49 (73개 바이트)
00 03 <= max start
00 02 <= max local
00 00 00 25 <= code length 37개
B2 00 02 12 03 B6
00 04 12 05 3C B2 00 02 BB 00 06 59 B7 00 07 12
08 B6 00 09 1B B6 00 0A B6 00 0B B6 00 04 B1 (37 bytes)
00 00 <= exception code length
00 01 <= attr count
00 11 <= 다시 attribute 17번 ref Line Number Table
00 00 00 12 <= attr length 18 bytes
00 04 <= line number length
00 00 <= start_pc
00 03 <= line num
00 08 <= start_pc
00 05 <= line num
00 0B <= start_pc
00 06 <= line num
00 24 <= start_pc
00 07 <= line num