본문 바로가기
💊 Java/- Java Lang

[조금 더 깊은 Java] Java Bytecode 를 알아보자 (자바를 컴파일하면 어떤 일이 일어날까?)

by Wonit 2021. 11. 25.

 

우리는 많은 시간 Java를 이용해서 다양한 소프트웨어를 개발하면서 들었던 소리가 있다.

 

Java는 JVM 이 있기 때문에 플랫폼에 종속적이지 않고 이식성이 뛰어나다.

 

그 이유에 대해서 생각해본 경험이 있는가?

 

오늘은 위의 JVM과 이식성을 이해하기 위해 꼭 알아야 하는 Java Bytecode 에 대해서 알아보려 한다.

 

Java Bytecode, 자바 바이트코드

 

우리는 Java 소프트웨어를 개발하기 위해서 JDK를 설치하고 Java 소프트웨어를 실행시키기 위해서 JRE 를 설치한다.

 

또한 개발을 하면 실행 결과를 확인하기 위해서 Compile 과정을 거치게 되는데, 이 컴파일은 바로 JDK나 JRE 에 함께 포함되는 javac.exe 실행파일이 수행하는 것이다.

 

이는 개발자가 작성한 .java 파일을 JVM이 이해할 수 있도록 하는 Bytecode 로 변환하고 .class 파일을 만드는 것을 의미하는데, .class 파일에 존재하는 데이터가 바로 자바 바이트코드, Java Bytecode 인 것이다.

 

주의해야 할 점이 컴파일 결과 라고 해서 C나 C++이 컴파일하면 생성하는 기계어와 동일하게 생각하면 안된다. 기계어는 JVM이 다른 모듈을 통해서 생성하고 실행하는데, 이 과정에서 C나 C++에 비해 조금 느린 성능을 내는 것이다.

 

Java Bytecode 는 우리가 개발한 자바 프로그램(코드)를 배포하는 가장 작은 단위라고 한다.

 

Java Bytecode 확인하기

 

더 깊은 이해를 위해서 우리가 직접 Java Bytecode 를 생성하고 확인해보자

 

다음과 같은 java 코드가 있다고 가정해보겠다.

 

public class Wonit {
  public static void main(String[] args) {
    String name = "워닉";
    int age = 25;

    Person blogger = new Person(name, age);

    blogger.print();
  }
}

class Person {
  String name;
  int age;

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  void print() {
    System.out.println("블로그 주인의 이름은 " + name + " 이며 나이는 " + age + " 이다");
  }
}

 

이제 이 코드를 컴파일해보자.

 

IDE를 통해서 컴파일을 해도 동일한 결과가 나오겠지만 지금 만큼은 javac.exe를 이용해서 직접 컴파일해보자!

 

$ javac Wonit.java

$ ls
Wonit.java    Wonit.class    Person.class

 

하나의 파일에서 작성했지만 우리는 2개의 클래스를 생성했기 때문에 결국 컴파일되는 코드는 2개의 클래스 파일이 생성되었다.

 

 

그리고 이 데이터를 HexD나 Hex Viewer 로 확인한다면 Byte 형태로 직접 확인할 수 있지만 속이 울렁거리므로 javap 로 디컴파일링을 하고 보기 편하게 바꿔보자.

 

$ javap -v -p -s Wonit.class

 

verbose 를 이용해서 디컴파일링하는 과정은 javap 를 실행하면 된다.

 

그렴 결과로 다음과 같은 분석 결과가 나오게 된다.

 

  minor version: 0
  major version: 58
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #17                         // Wonit
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
public class Wonit
  minor version: 0
  major version: 58
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   ... 생략
  #24 = Utf8               Wonit.java
{
  public Wonit();
    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 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    Code:
      stack=4, locals=4, args_size=1
         0: ldc           #7                  // String 워닉
         5: istore_2
        ... 생략
        20: return
      LineNumberTable:
        line 3: 0
        line 4: 3
        line 6: 6
        line 8: 16
        line 9: 20
}
SourceFile: "Wonit.java"

 

사실상 이 코드들을 완벽하게 이해하는 것은 내 수준으로 도저히 불가능하더라.. 그래도 최대한 이해하려고 해보았는데, 그 결과를 이제 함께 공유하려 한다.

 

Java Bytecode 의 구성요소

 

자바 바이트코드의 구성요소는 많이 있지만 크게 3가지가 존재한다고 보자

 

  1. Class Format
  2. Type의 표현
  3. Constant Pool
  4. Instruction Set

 

Class Format

 

위의 자바 바이트코드는 나름의 포맷이 정해져있고, 해당 포맷으로 표현이 된다.

 

우리가 컴파일한 코드는

 

minor version: 0
major version: 58
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #17                         // Wonit
super_class: #2                         // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 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];  
}

 

  • magic
    • 클래스 파일의 첫 4바이트로 자바 클래스파일이 맞는지 구분하는 용도로 쓰인다. PE Header 나 Image Signatur 과 같은 용도라고 보고 CAFEBABE 라는 이름을 갖고 있다.
  • minor_version, major_version
    • 클래스의 Version 을 나타낸다. 즉, JDK 1.6이냐 1.8이냐 를 구분하는데, 각각 JDK 버전에 따라 다른 수가 나오게 된다
  • constant_pool_count
    • 클래스 파일의 상수 풀(Constant Pool) 의 갯수를 나타내는 용도로 사용된다.
  • access_flags
    • 주로 클래스의 public, final 과 같은 modifier 정보를 나타낸다.
  • interface_count
    • 클래스가 구현한 인터페이스의 개수와 각 인터페이스에 대한 constant_pool 내의 인덱스를 나타낸다.

 

위에 설명한 포맷 말고도 다른 포맷의 정보도 분명 존재하겠지만 이들을 설명하는 것은 오히려 이 포스팅의 목적을 잃는 것이라 판단이 되어 생략하도록 하겠다.

 

사실 내가 이해를 하지 못하였기 때문에

 

Type의 표현

 

자바 바이트코드의 표현은 우리가 사용할 수 있는 모든 Type 을 Bytecode Expression 으로 변환할 수 있다.


대표적인 타입은

 

  • B : byte
  • C : char
  • I : int
  • L<classname>; : reference

 

정도가 있다.

 

예를 들어서 다음과 같은 코드가 있다면

 

Object print(String str, int i)

 

Type 의 표현으로 다음과 같이 표현할 수 있다.

 

(java/lang/String;I)Ljava/lang/Object;

 

더욱 구체적인 타입의 표현은 oracle java bytecode docs를 참고하면 된다.

 

Chapter 4. The class File Format

The target of each jump and branch instruction (jsr, jsr_w, goto, goto_w, ifeq, ifne, ifle, iflt, ifge, ifgt, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmple, if_icmplt, if_icmpge, if_icmpgt, if_acmpeq, if_acmpne) must be the opcode of an instruction wi

docs.oracle.com

 

Constant Pool

 

JVM은 Host OS 의 메모리를 최대한 효율적으로 이용하도록 설계가 되어있다.

 

이를 위해서 JVM은 Constant Pool 이라는 전략을 사용하는데, JVM이 동적으로 코드를 실행시킬 때 모든 데이터를 즉시 생성하는 것이 아니라 Constant Pool 에 저장하고 Constant Pool 에 존재하는 데이터를 우선적으로 가져와 메모리를 더욱 효율적으로 사용할 수 있게 되는 것이다.

 

# 형태의 해시코드로 시작하는 것이 특징이다.

 

이 Constant Pool에 대해서는 해당 블로그의 조금 더 깊은 Java - String Constant Pool 에서 확인할 수 있다.

 

[조금 더 깊은 Java] String 과 String Constant Pool

우리는 Java 를 사용하면서 아주 많은 String 의 Literal 을 이용하게 된다. 오늘은 그 String이 어떤 특성을 가졌는지 조금 다른 각도에서 봐보려 한다. String 은? String 은 Java 에서 제공하는 특별한 자료

wonit.tistory.com

 

Instruction Set

 

자바 바이트코드는 컴파일된 결과로 생성되는 코드 이므로 일종의 명령어 집합이라고 할 수 있다.

 

이를 JVM Instruction Set 이라고 한다.

 

명령어는 당연하게 OpCode와 Operands 로 구성되는데 자바 바이트코드에서는 1 Byte의 OpCode와 2 Byte의 Operands 로 구성된다.

 

1 Byte의 OpCode 이므로 사용가능한 총 명령어의 수는 256개가 되고 자세한 명령어의 정의는 Oracle Java Bytecode Instruction Set Docs 에서 확인할 수 있다.

 

Chapter 6. The Java Virtual Machine Instruction Set

The wide instruction modifies the behavior of another instruction. It takes one of two formats, depending on the instruction being modified. The first form of the wide instruction modifies one of the instructions iload, fload, aload, lload, dload, istore,

docs.oracle.com

 

우리가 디컴파일한 결과에서 찾아볼 수 있다.

 

Code:
      stack=4, locals=4, args_size=1
         0: ldc           #7                  // String 워닉
         2: astore_1
         3: bipush        25
         5: istore_2
         6: new           #9                  // class Person
         9: dup
        10: aload_1
        11: iload_2
        12: invokespecial #11                 // Method Person."<init>":(Ljava/lang/String;I)V
        15: astore_3
        16: aload_3
        17: invokevirtual #14                 // Method Person.print:()V
        20: return

 

우리가 짠 소스코드의 자바 바이트코드를 분석하기 위해서 알아야할 몇가지 명령어를 알아보자

 

  • aload : local variable 을 stack 에 push 한다
  • ldc : constant pool 에서부터 #index 에 해당하는 데이터를 가져온다
  • astore : local variable 에 값을 저장한다.
  • invokespecial : instance Method 를 호출하고 결과를 stack 에 push한다.
  • new : 새로운 객체를 생성한다.
  • invokevirtual : 메서드를 호출한다.
  • dup : stack 에 있는 top 을 복사한다.

 

이외에도 정말 많은 명령어가 존재하고 의미가 존재하는데, 한글로된 설명은 aroundck 님의 블로그 에서 확인할 수 있다.

댓글0