728x90
반응형

앞선 👉에서 JDK에 대해 알아보았다.
이제 본 글에서는 Java 프로그램을 구동시켜주는 JVM에 대해 상세히 알아보고자 한다.

위 사진은 JVM 아키텍처를 나타낸다.
예제코드를 작성하고, JVM이 해당 예제코드를 어떻게 실행시키는지 보면서 위 아키텍처를 설명해보겠다.

예제 세팅

예제코드는 아래와 같다.

  • Main.java
public class Main {
    public static void main(String[] args) {
        Woong woong = new Woong();
        woong.methodA(3);
    }
}
  • Woong.java
public class Woong {

    public int methodA(int param) {
        int localVariable = 1;
        int sum = localVariable + param;
        methodB();
        return sum;
    }

    private void methodB() {

    }
}

위 Java코드를 컴파일하여 class파일을 생성해보겠다.
아래 명령어를 실행하면 Main.classWoong.class 두 개의 클래스 파일이 생성될 것이다.
javac Main.java Woong.java

생성된 클래스 파일을 HexDHex Viewer로 보면 직접 Byte형태로 볼 수 있지만,
사람이 이해하기 어려운 문법이므로 역어셈블이라는 과정을 거쳐 사람이 이해하기 쉬운 형태로 변환해보겠다.
javap -v -p -s Main.class, javap -v -p -s Woong.class

그리고 아래는 확인할 수 있는 형태의 바이트 코드이다.

  • Main.class
Classfile /Users/daewoong/JavaStudy/src/Main.class
  Last modified 2023. 7. 23.; size 318 bytes
  SHA-256 checksum de5de7f896a34cb4c31a490af4edcf325015e7050ecbb9d0bfeb3bda7854859d
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 63
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #14                         // Main
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, 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 = Class              #8             // Woong
   #8 = Utf8               Woong
   #9 = Methodref          #7.#3          // Woong."<init>":()V
  #10 = Methodref          #7.#11         // Woong.methodA:(I)I
  #11 = NameAndType        #12:#13        // methodA:(I)I
  #12 = Utf8               methodA
  #13 = Utf8               (I)I
  #14 = Class              #15            // Main
  #15 = Utf8               Main
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               Main.java
{
  public Main();
    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
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #7                  // class Woong
         3: dup
         4: invokespecial #9                  // Method Woong."<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_3
        10: invokevirtual #10                 // Method Woong.methodA:(I)I
        13: pop
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 8
        line 5: 14
}
SourceFile: "Main.java"
  • Woong.class
Classfile /Users/daewoong/JavaStudy/src/Woong.class
  Last modified 2023. 7. 23.; size 322 bytes
  SHA-256 checksum 9709d10e0bfcde2dd1b9477a8de2f210e322181f9f42835d3c3898b7e213f904
  Compiled from "Woong.java"
public class Woong
  minor version: 0
  major version: 63
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // Woong
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, 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 = Methodref          #8.#9          // Woong.methodB:()V
   #8 = Class              #10            // Woong
   #9 = NameAndType        #11:#6         // methodB:()V
  #10 = Utf8               Woong
  #11 = Utf8               methodB
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               methodA
  #15 = Utf8               (I)I
  #16 = Utf8               SourceFile
  #17 = Utf8               Woong.java
{
  public Woong();
    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 int methodA(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: iconst_1
         1: istore_2
         2: iload_2
         3: iload_1
         4: iadd
         5: istore_3
         6: aload_0
         7: invokevirtual #7                  // Method methodB:()V
        10: iload_3
        11: ireturn
      LineNumberTable:
        line 4: 0
        line 5: 2
        line 6: 6
        line 7: 10

  private void methodB();
    descriptor: ()V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 12: 0
}
SourceFile: "Woong.java"

위 바이트코드는 크게 세 가지 정보로 분류된다.

  • 클래스 정보
  • Constant Pool
  • Instruction Set

우리는 Constant PoolInstruction Set에만 집중하여 JVM을 파악해볼 것이다.
자세한 바이트 코드 정보는 Oracle Doctistory 글에서 확인할 수 있다.

JVM 동작 원리

JVM의 목적 : 바이트코드 형태의 Class 파일을 컴퓨터가 읽을 수 있는 기계어로 번역하여 CPU에 명령을 내림

Java 프로그램이 실행되면 JVM은 먼저 Class Loader를 통해 Class파일을 읽는다.
Class Loader에 의해 읽혀진 Class파일은 검증과정과 초기화과정(static 변수 초기화 등)을 거쳐 Runtime Data AreasMethod Area라는 메모리 공간에 올려진다.

메모리 공간에 올려진 Class 파일의 바이트코드는 Execution EngineInterpreterJIT Compiler에 의해 기계어로 번역되어 CPU에 전달된다. 그리고 기계어로 번역될 프로그램 동작과 관련한 정보는 Runtime Data Areas 저장되고, 실시간으로 저장된 정보가 인터프리터에 의해 기계어로 번역되어 CPU로 전달될 것이다.

이제 Runtime Data Areas의 원리에 집중하면서 JVM의 동작을 살펴볼 것이다.

Runtime Data Areas

Runtime Data Areas는 크게 다섯가지 공간으로 분류된다.

  1. Method Area : 클래스에 대한 정보 저장 (스레드 공유 공간)
  2. Heap : 런타임에 생성되는 모든 객체들의 대한 정보 저장 (스레드 공유 공간)
  3. JVM Stacks : 메서드를 실행하기 위한 정보들이 저장되는 공간, Frame 자료구조 활용 (스레드당 1개)
  4. PC Registres : 현재 실행되고 있는 명령의 주소를 저장 (스레드당 1개)
  5. Native Method Stacks : C나 C++로 작성된 메서드를 실행할 때 사용되는 Stack (스레드당 1개)

JVM Stacks

Class의 메인 메서드가 실행되거나 스레드가 생성되면 JVM Stacks은 하나씩 생성된다.

JVM Stacks은 위 사진처럼 구성되어 있다.
생성된 스레드에서 메서드가 호출될 때마다 메서드 동작에 대한 정보가 Frame 자료구조 형태로 생성되어 JVM Stacks에 쌓인다.
Frame형태로 쌓인 메서드의 동작이 끝나거나 Exception이 발생하면 해당 Frame은 pop된다.

Frame 자료구조에 대해 자세히 알아보자.

  • Local Variables Array : 실행된 메서드의 지역변수의 공간이 해당 배열에 생성된다. 선언된 순서대로 1번 인덱스부터 할당된다. 0번 인덱스는 this로 자기자신을 가리킨다.
  • Operand Stack : Instruction Set에 따른 피연산값 및 연산의 중간값들을 저장하는 Stack
  • Constant Pool : 클래스 내에서 사용되는 상수(constant)들을 담은 테이블

Constant Pool은 Class 파일 상의 Constant Pool의 데이터를 가리킨다.
아래는 Main.class의 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 = Class              #8             // Woong
   #8 = Utf8               Woong
   #9 = Methodref          #7.#3          // Woong."<init>":()V
  #10 = Methodref          #7.#11         // Woong.methodA:(I)I
  #11 = NameAndType        #12:#13        // methodA:(I)I
  #12 = Utf8               methodA
  #13 = Utf8               (I)I
  #14 = Class              #15            // Main
  #15 = Utf8               Main
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               Main.java

#2 = Class #4로 예를들면,
#2 = Class 부분은 Index 및 Type을 가리키고 #4는 참조값을 가리킨다.
#2인덱스는 #4인덱스를 참조하고 있고, #4인덱스는 java/lang/Object를 가리키고 있으므로,
#2인덱스는 java/lang/Object를 가리키고 있다고 봐도 무방하다.

Instruction Set에 따른 JVM 동작

JVM 아키텍처의 각 모듈에 대해 알아보았으니, 이제 동작원리를 알아볼 것이다.

  1. 클래스로더에 의해 Class 파일이 Method Area에 올라온다. (스레드가 실행되면서 필요한 클래스들은 필요할 때마다 동적으로 클래스로더에 의해 불려진다.)
  2. JVM은 Class 파일을 해석하여 Instruction Set의 순서대로 프로그램을 동작시킨다.
         0: new           #7                  // class Woong
         3: dup
         4: invokespecial #9                  // Method Woong."<init>":()V
         7: astore_1
         8: aload_1
         9: iconst_3
        10: invokevirtual #10                 // Method Woong.methodA:(I)I
        13: pop
        14: return
         0: iconst_1
         1: istore_2
         2: iload_2
         3: iload_1
         4: iadd
         5: istore_3
         6: aload_0
         7: invokevirtual #7                  // Method methodB:()V
        10: iload_3
        11: ireturn

주요 Instruction Set만 알아볼 것이다.

  • new : Constant Pool의 #7 인덱스에 해당하는 클래스의 인스턴스를 생성한다. Method Area상의 해당 클래스의 사이즈를 계산하고 Heap 메모리를 할당한다. 그리고 할당된 Heap 메모리에 대한 참조값이 지역변수에 저장된다. (Method Area에 해당 클래스가 없을 경우, 클래스 로더에 의해 Method Area로 해당 클래스가 불려진다.)
  • iconst_1 : 정수값 1을 Operand Stack에 올린다.
  • istore_2 : Operand Stack에서 값을 꺼내서 Local Variables Array의 2번 인덱스에 저장한다.
  • iload_2 : Local Variables Array의 2번 인덱스 값을 Operand Stack에 올린다.
  • iadd : Operand Stack상단 두 값을 더한 후에 다시 Operand Stack에 저장한다.
  1. Instruction Set의 각 명령어마다 인터프리터에 의해 기계어로 번역되어 실시간으로 실행된다.

클래스 로더에 의해 로드된 Class 파일은 JVM에 의해 위 Flow대로 실행된다.

Garbage Collection

GC(Garbage Collection)는 JVM 메모리를 자동으로 관리해준다. 객체 생명주기에 따라서 자동으로 메모리를 해제시켜주는데,
해당 GC의 원리는 다음 연재될 글에서 살펴보겠다.

반응형

+ Recent posts