내가 만든 자바 코드가 어떤 과정으로 실행되는지 알아보기 위한 "JVM 알아보기" 세 번째 포스팅입니다.

이번 포스팅에서는 JVM 메모리 구조에 대해서 알아보겠습니다.

 


JVM Architecture

 

지난 포스팅에서 이야기한 것과 같이, ClassLoader 는 ".class" 파일에서 바이너리 데이터를 생성하여 Method Area 에 저장한다.  

JVM Architecture 그림을 보면 Method Area 는 Runtime Data Areas 의 한 부분이다.

JVM 은 OS 로부터 프로그램 실행 중에 사용되는 메모리 영역을 할당받는데, 이를 Runtime Data Areas 라고 한다.

 

 

앞으로 설명할 Method Area 나 Heap Area 는 JVM 의 스펙에 정의되어 있는 논리적 개념이다.

모든 JVM 에 Method Area 가 있지만, "여기가 Method Area 입니다." 라는 것이 아니라

JVM 스펙에 따라 정해진 역할을 하는 영역을 Method Area 라고 논리적 개념으로 설명하는 것이다.

(참고) Is Method area still present in Java 8?

 

 

 

 

 Runtime Data Areas

 

Runtime Data Areas 의 공유 자원으로 Heap Area 와 Method Area 가 있다.

이는 모든 Thread 가 이 영역을 공유한다는 의미이며, 모든 Thread 가 공유하기 때문에 동기화 문제가 발생할 수 있다는 의미이다.

 

 

Heap Area

 

힙 영역은 JVM 에서 가장 중요한 메모리 영역이다.

실제 객체를 생성(메모리 할당) 및 저장하는 공간으로, JVM 이 시작될 때 생성된다.

 

다음과 같이 new 키워드를 사용해서 객체를 생성하면 JVM 이 객체의 인스턴스를 힙에 생성한다.

StringBuilder builder = new StringBuilder();

힙 영역에 생성된 객체와 배열은 스택 영역의 변수나 다른 객체의 필드에서 참조한다.

 

실행 중인 JVM 프로세스에는 단 하나의 Heap 영역만 있으며, GC(Garbage Collection) 의 대상이 되는 영역이다.

객체가 더 이상 사용되지 않거나 명시적으로 null 로 선언하면 의미 없는 객체가 되어 GC 의 대상이 된다.

 


 

Method Area

 

메서드 영역에는 클래스 코드, 클래스 이름, 부모 클래스 이름, 메서드, 생성자, 변수 정보 등 모든 클래스 수준 정보가 저장 된다.

이를 클래스와 클래스의 메타 데이터를 저장한다고 말한다.

 

메서드 영역에는 Runtime Constant Pool 이라는 집합이 존재한다.

리터럴(문자열, 정수 및 부동 소수점 상수) 및 필드, 메서드에 대한 심볼릭 레퍼런스*를 저장하며, 이후에 배울 Stack 의 Frame 에서 참조한다.

 

*심볼릭 레퍼런스란?

참조하는 대상의 이름. 사용 전에는 이름만 저장되어 있다.

클래스 접근 필요 시 클래스 로더가 Linking 하여 메모리 상의 실제 주소로 연결된다. (동적으로 로드)

이를 Resolution 이라고 부른다.

 

Constant Pool 을 확인하기 위해 다음과 같이 간단한 클래스를 생성해보자.

package ex;

public class Student {
    private int age = 29;

    public void study() {
        System.out.println("studying....");
    }
}
package ex;

public class School {
    public static void main(String[] args) {
        Student student = new Student();
        student.study();
    }
}

 

School 클래스를 역어셈블해보자.

javap -v [Class Name]

 

아래와 같이 Constant pool 을 포함한 클래스 파일의 여러 정보를 확인할 수 있다.

 

 

역어셈블링하여 확인한 School 클래스 파일의 Constant pool 은 다음과 같다.

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             // ex/Student
   #8 = Utf8               ex/Student
   #9 = Methodref          #7.#3          // ex/Student."<init>":()V
  #10 = Methodref          #7.#11         // ex/Student.study:()V
  #11 = NameAndType        #12:#6         // study:()V
  #12 = Utf8               study
  #13 = Class              #14            // ex/School
  #14 = Utf8               ex/School
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lex/School;
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Utf8               args
  #23 = Utf8               [Ljava/lang/String;
  #24 = Utf8               student
  #25 = Utf8               Lex/Student;
  #26 = Utf8               SourceFile
  #27 = Utf8               School.java

 

#(해시) 로 참조하고 있는 상수 풀의 인덱스를 표시한다.

인덱스와 타입, 그리고 매핑하는 값을 가지고 있다.

 

#8 = Utf8               ex/Student

 

#8 을 보면 참조하는 대상의 이름을 문자열로 가지고 있다. 이를 심볼릭 레퍼런스를 저장한다고 한다.

이처럼 Constant pool 은 모든 타입, 필드, 메서드에 대한 심볼릭 레퍼런스를 저장하고 있기 때문에 자바 프로그램에서 중요한 역할을 한다.

 


 

 

다음으로는 Thread 마다 1개씩 존재하는 Java Stack, PC(Program Counter) Register, Native Method Stack 에 대해서 알아보자.

Thread 별로 메모리를 할당하기 때문에 동시성 문제에서 자유롭다.

 

 

 

Java Stack

 

각 Thread 에는 Thread 와 동시에 할당된 Java Stack 이 있다.

메서드 호출 시 필요한 수행 정보를 저장하는 역할로, 메서드 정보와 지역 변수, 매개변수, 연산 중 발생하는 임시 데이터를 저장한다.

기본 타입 변수는 스택 영역에 직접 값을 가지며, 참조 타입 변수는 힙 영역이나 메서드 영역의 객체 주소를 가진다.

 

스택 영역에서는 메서드 호출 시 필요한 정보를 저장하기 위해 Frame 을 사용한다.

메서드가 호출 될때마다 각각의 스택 프레임이 생성되는데, 각 프레임은 하나의 메서드에 대한 정보를 저장한다.

메서드를 호출할 때마다 프레임을 추가하고, 메서드가 종료되면 해당 프레임을 제거한다.

 

Java Stack 의 구조를 그림으로 표현하면 다음과 같다.

 

출처:https://8iggy.tistory.com/229

 

예시를 통해 확인해보자.

 

public class test {
    public static void main(String[] args) {
        int num1 = 1;
        int num2 = 2;
        int num3 = 3;

        num1 = num1 + num2 + num3;
    }
}

 

역어셈블링하여 바이트코드를 확인해보면 다음과 같다.

 0: iconst_1
 1: istore_1
 2: iconst_2
 3: istore_2
 4: iconst_3
 5: istore_3
 6: iload_1
 7: iload_2
 8: iadd
 9: iload_3
10: iadd
11: istore_1
12: return

 

한줄씩 설명하면 아래와 같다.

 

이렇게 바이트코드 0~5행까지 num1, num2, num3 을 Local Variable Array 에 저장하고

6행부터는 Local Variable Array 에서 값을 가져와서 연산을 수행한다.

 

 

 

메서드의 지역 변수는 Local Variables 라는 배열에 저장이 되고, 연산이 필요한 경우 Operand Stack(피연산자 스택)에 쌓아 연산 후 Local Variables 배열에 최종 값을 저장한다.

변수가 객체인 경우, Local Variables 에 Heap 에 저장된 인스턴스의 참조 값이 저장되게 된다.


 

PC Register

 

쓰레드가 각각의 메서드를 수행할 때, 각각의 명령어 주소값을 저장하는 공간이다.

쓰레드마다 PC Register 를 가지고 있으며, JVM 이 현재 수행 중인 명령어의 주소를 저장한다.

지금 실행하고 있는 메서드의 몇 번째 줄을 실행해야 하는지 나타낸다. 

 


 

Native Method Stacks

 

Java 가 아닌 다른 언어로 작성 된 메서드를 위한 메모리 영역이다.

 

 

 

 

마치며

오늘은 JVM 의 메모리 구조 중에서도 Runtime Data Area 의 구성요소에 대해 자세하게 알아보았습니다.

다음 포스팅에는 Heap 의 구조과 GC 를 다루도록 하겠습니다.

 

 

Reference

복사했습니다!