본문 바로가기

Java

JVM 내부 구조 (2) - 런타임 데이터 영역

JVM 메모리 영역 - Runtime Data Area


 

  • 자바 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받고, 그 메모리를 여러 영역으로 나누어 관리함
  • JVM 메모리 영역 "Runtime Data Area"는 크게 세 가지 영역으로 구분할 수 있음  - 메소드 영역, 힙 영역, 스택 영역
  • 메소드 영역과 힙 영역은 모든 스레드가 공유하는 영역
  • 스택 영역과 기타 영역들은 스레드별로 생성되는 영역

 

메소드 영역 (static 영역)


  • 클래스 로더에 의해 클래스가 로딩될 때 생성
  • 클래스 로더가 읽어 들인 클래스에 대한 런타임 상수 풀, 필드 데이터, 메소드 데이터 등을 저장
  • 모든 스레드가 공유하는 영역이기 때문에 공유 데이터의 경우 thread safe 해야 함

클래스 파일 구조

컴파일된 클래스 파일은 아래와 같이 구성된다.

public class MyProgram {

    private static final int INFINITY = Integer.MAX_VALUE;
    private static final double PI = Math.PI;
    private static final String author = "JO";

    public static void main(String[] args) {
        int sum = sum(5, 10);

        System.out.println(sum);
    }

    public static int sum(int a, int b) {
        int sum = a + b;

        return sum;
    }
}

 

// javap -v -p -s -sysinfo -constants MyProgram

Classfile ....../src/main/java/MyProgram.class
  Last modified 2023. 11. 28.; size 654 bytes
  SHA-256 checksum e5e846fdfbb9ceb1f31e39aa7d92ff601ae85d9709a3c58ab8f5c6a3100726ec
  Compiled from "MyProgram.java"
public class MyProgram
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #12                         // MyProgram
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 3, methods: 3, attributes: 1
Constant pool:
   #1 = Class              #2             // java/lang/Integer
   #2 = Utf8               java/lang/Integer
   #3 = Class              #4             // java/lang/Math
   #4 = Utf8               java/lang/Math
   #5 = Methodref          #6.#7          // java/lang/Object."<init>":()V
   #6 = Class              #8             // java/lang/Object
   #7 = NameAndType        #9:#10         // "<init>":()V
   #8 = Utf8               java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Methodref          #12.#13        // MyProgram.sum:(II)I
  #12 = Class              #14            // MyProgram
  #13 = NameAndType        #15:#16        // sum:(II)I
  #14 = Utf8               MyProgram
  #15 = Utf8               sum
  #16 = Utf8               (II)I
  #17 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
  #18 = Class              #20            // java/lang/System
  #19 = NameAndType        #21:#22        // out:Ljava/io/PrintStream;
  #20 = Utf8               java/lang/System
  #21 = Utf8               out
  #22 = Utf8               Ljava/io/PrintStream;
  #23 = Methodref          #24.#25        // java/io/PrintStream.println:(I)V
  #24 = Class              #26            // java/io/PrintStream
  #25 = NameAndType        #27:#28        // println:(I)V
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (I)V
  #29 = Utf8               INFINITY
  #30 = Utf8               I
  #31 = Utf8               ConstantValue
  #32 = Integer            2147483647
  #33 = Utf8               PI
  #34 = Utf8               D
  #35 = Double             3.141592653589793d
  #37 = Utf8               author
  #38 = Utf8               Ljava/lang/String;
  #39 = String             #40            // JO
  #40 = Utf8               JO
  #41 = Utf8               Code
  #42 = Utf8               LineNumberTable
  #43 = Utf8               main
  #44 = Utf8               ([Ljava/lang/String;)V
  #45 = Utf8               SourceFile
  #46 = Utf8               MyProgram.java
{
  private static final int INFINITY = 2147483647;
    descriptor: I
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: int 2147483647

  private static final double PI = 3.141592653589793d;
    descriptor: D
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: double 3.141592653589793d

  private static final java.lang.String author = "JO";
    descriptor: Ljava/lang/String;
    flags: (0x001a) ACC_PRIVATE, ACC_STATIC, ACC_FINAL
    ConstantValue: String JO

  public MyProgram();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #5                  // 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: iconst_5
         1: bipush        10
         3: invokestatic  #11                 // Method sum:(II)I
         6: istore_1
         7: getstatic     #17                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #23                 // Method java/io/PrintStream.println:(I)V
        14: return
      LineNumberTable:
        line 8: 0
        line 10: 7
        line 11: 14

  public static int sum(int, int);
    descriptor: (II)I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_2
         5: ireturn
      LineNumberTable:
        line 14: 0
        line 16: 4
}
SourceFile: "MyProgram.java"

 

JVM 내부에서 사용되는 타입 표기자는 아래와 같다.

B   			byte       	부호가 있는 8비트 정수
C   			char       	UTF-16 인코딩된 유니코드 문자
D   			double     	64비트 부동 소수점
F   			float      	32비트 부동 소수점
I   			int        	32비트 정수
J   			long       	64비트 정수
S   			short      	부호가 있는 16비트 정수
V			void		반환 타입 없음
Z   			boolean    	논리값
[   			reference  	1차원 배열 참조
LClassName;		reference  	'ClassName' 클래스의 인스턴스 참조

 

예를 들어, ([Ljava/lang/String;)V 의 경우 리턴 타입이 void이고, 매개변수로 Sring[] 타입을 받음을 의미한다.

즉, public static void main(String[] args) 메소드의 시그니처라고 볼 수 있다.

메소드 영역에 저장되는 데이터

  • 클래스 정보
  • 런타임 상수 풀
  • 필드 데이터 - 필드명, 타입, 접근 제어자 등
  • 메소드 데이터 - 메소드 명, 리턴 타입, 파라미터 타입, 접근 제어자 등
  • 메소드 코드 - 바이트 코드, Operand stack size, Local variable size, Local variable table 등

런타임 상수 풀 (Runtime Constant Pool)

Java 바이트 코드가 실행될 때 데이터가 필요하며, 이 데이터를 바이트 코드에 직접 저장하기에는 너무 크다. 따라서 데이터는 상수 풀에 저장되고, 바이트 코드는 상수 풀을 참조한다. 또한, 런타임 상수 풀은 동적 링킹에 사용된다.

동적 링킹

C/C++에서는 일반적으로 실행에 필요한 여러 파일을 Linking 과정에서 함께 컴파일되어 dll과 같은 아티팩트를 생성하는 반면, Java에서는 이러한 과정을 런타임에 동적으로 수행된다. (JVM 구현에 따라 참조 시기가 달라질 수 있음)

Java 코드가 컴파일될 때 바이트 코드가 참조하는 변수나 메소드는 상수 풀에 있는 심볼릭 레퍼런스, 즉 가상의 논리적인 위치이다. 클래스 로더에 의해 로드된 후 Verifying 단계 또는 바이트 코드가 실행되어 심볼릭 레퍼런스가 처음 사용될 때 실제 해당 데이터가 위치를 참조하게 된다.

 

 

런타임 상수 풀에는 아래 유형의 데이터를 포함한다. (위 클래스 파일의 일부 코드를 예시로 참조함)

 

Integer 4 바이트 정수 리터럴  
Long 8 바이트 정수 리터럴  
Float 4 바이트 소수 리터럴  
Double 8 바이트 소수 리터럴  
Utf8 Utf8 인코딩된 문자열 리터럴 #40 = Utf8                       JO
String 상수 풀에 정의된 다른 Utf8 문자열을 가르키는 진입점 #39 = String                    #40
Class 상수 풀에 정의된 Utf8 클래스명 가르키는 진입점 #12 = Class                      #14
..
#14 = Utf8                       MyProgram
NameAndType 콜론으로 구분된 두 값을 가지며, 두 값은 각각의 진입점을 가르킴

콜론 앞의 값은 메소드명 또는 필드명 나타내는 Utf8

콜론 뒤의 값은 매개변수와 리턴 타입  또는 클래스명을 나타내는 Utf8
#13 = NameAndType      #15:#16
..
#15 = Utf8                        sum
#16 = Utf8                       (II)I
Fieldref
Methodref
InterfaceMethodref
(.) 으로 구분된 두 값을 가지며, 두 값은 각각의 진입점을 가르킴

콜론 앞의 값은 Class 를 가르킴

콜론 뒤의 값은 NameAndType 를 가르킴
#11 = Methodref              #12.#13
#12 = Class                      #14
#13 = NameAndType      #15:#16
#14 = Utf8                       MyProgram
#15 = Utf8                       sum
#16 = Utf8                       (II)I

 

 

힙 영역


 

  • 런타임에 클래스 인스턴스와 배열을 할당하는 데 사용
  • 스택 프레임은 생성된 후 크기가 변하지 않기 때문에 동적으로 크기가 변경될 수 있는 배열과 객체는 스택에 저장될 수 없음
  • 스택 프레임은 힙에 저장된 인스턴스나 배열을 가리키는 참조만 저장
  • 스택 프레임에 저장되는 데이터와 달리 인스턴스는 항상 힙에 저장되어 메소드가 끝나더라도 제거되지 않기 때문에 GC에 의해 제거
  • 모든 스레드가 공유하는 영역이기 때문에 공유 데이터의 경우 thread safe 해야 함

 

힙 메모리 관리

인스턴스와 배열은 명시적으로 메모리 상에서 제거되지 않는 대신 Garbage Collector가 자동으로 메모리를 회수한다.

 

Java 버전 별로 힙 영역을 구분하는 기준과 GC 기법이 상이하기 때문에 추후 작성 예정

 

스택 영역


  • 각 스레드에는 해당 스레드에서 실행되는 메소드에 대한 정보를 담고 있고, 이를 '프레임' 이라고 함
  • 스택은 LIFO 데이터 구조이므로 현재 실행 중인 메소드가 스택의 최상단 프레임에 위치
  • 메소드를 호출할 때마다 새 프레임이 생성되어 스택에 추가
  • 메소드가 종료되거나 예외 발생 시 프레임이 제거(Pop)됨
  • 일반적으로 스택 영역의 사이즈는 고정되어 있기 때문에 스레드에 할당된 스택보다 더 큰 스택이 필요한 경우 StackOverflowError 발생
  • 새로운 스레드에 프레임을 할당할 메모리가 충분하지 않다면 OutOfMemory 발생
  • 프레임은 네 가지 요소로 구성됨 - Local Variable Array, Operand Stack, Return Value, Runtime Contants Pool Reference

지역 변수 배열 (Local Variable Array)

  • this에 대한 참조, 메소드 매개변수, 지역 변수를 포함한 메소드 실행 중에 사용되는 모든 변수를 포함
  • static 메소드의 경우 배열의 0번째 슬롯부터 저장되고, 인스턴스 메소드의 경우 0번째 슬롯은 this로 지정
  • 지역 변수로 등록되는 타입은 boolean, byte, char, short, int, long, float, double, reference, returnAddress 로 구성
  • long과 double 타입의 데이터는 두개의 연속된 슬롯(32bit * 2)에 저장되고, 나머지 타입은 하나의 슬롯을 차지

피연산자 스택 (Operand Stack)

  • 바이트 코드 명령어를 실행하는 동안 사용
  • 바이트 코드 명령어는 push, pop, duplicating, swapping, execution 작업을 통해 피연산자 스택을 사용
  • 따라서 지역 변수 배열과 피연산자 스택 간에 값을 이동하는 명령어가 매우 자주 발생

지역 변수 배열과 피연산자 스택 예시

public class MyProgram {

    public static void main(String[] args) {
        int sum = sum(5, 10);

        System.out.println(sum);
    }

    public static int sum(int a, int b) {
        int sum = a + b;
        
        return sum;
    }
}

 

아래 코드는 위 자바 코드를 컴파일한 바이트 코드와 지역변수 배열을 의미한다.

  public static void main(java.lang.String[]);
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_5
         1: bipush        10
         3: invokestatic  #7                  // Method sum:(II)I
         6: istore_1
         7: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: iload_1
        11: invokevirtual #19                 // Method java/io/PrintStream.println:(I)V
        14: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
            7       8     1   sum   I

  public static int sum(int, int);
    Code:
      stack=2, locals=3, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: istore_2
         4: iload_2
         5: ireturn
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0     a   I
            0       6     1     b   I
            4       2     2   sum   I

 

스택 프레임 바이트 코드   Operand Stack 비고
[main] iconst_5 정수 리터럴 5 push [5] main 스택 프레임 생성
[main] bipush 10 정수 리터럴 10 push [5, 10]  
[main] invokestatic #7 #7 sum:(II)I 메소드 호출 [] 두개의 매개변수를 가지므로 pop 두번 후, 매개변수로 입력
[main, sum] iload_0 Local Variable Array 슬롯 0의 값 push [5] sum 스택 프레임 생성
[main, sum] iload_1 Local Variable Array 슬롯 1의 값 push [5, 10]  
[main, sum] iadd 스택의 두 값을 pop
더한 값을 다시 push
[15]  
[main, sum] istore_2 스택에서 pop
Local Variable Array 슬롯 2에 저장
[]  
[main, sum] iload_2 Local Variable Array 슬롯 2의 값 push [15]  
[main, sum] ireturn 스택에서 pop한 값 return
sum 메소드 종료
[] sum 스택 프레임 제거
[main] invokestatic #7 sum에서 return 받은 값을 push [15] invokestatic은 return 값을 push하는 것까지임
[main] istore_1 스택에서 pop
Local Variable Array 슬롯 1에 저장
[]  
[main] getstatic #13 #13(java/lang/System.out)의 정적 필드(PrintStream) push [System.out.PrintStream]  
[main] iload_1 Local Variable Array 슬롯 1의 값 push [System.out.PrintStream, 1]  
[main] invokevirtual #19 #19 println:(I)V 메소드 호출
[] 한개의 매개변수를 가지므로 pop 한번 후, 매개변수로 입력

메소드를 호출하기 위해 System.out.PrintStream을 pop

리턴 타입이 V(void)기 때문에 스택 변화 없음
[main] return main 메소드 종료 []  
[]     [] main 스택 프레임 제거

 

바이트 코드의 실행 흐름은 위와 같다.

 

메소드를 호출할 때마다 해당 메소드에 대한 스택 프레임이 추가되고, 해당 메소드의 바이트 코드가 실행된다. 위 예시에선 볼드 처리된 스택 프레임이 현재 실행 중인 메소드이다.

 

iconst_<N>과 bipush N

두 명령어 모두 정수 리터럴을 스택에 push 연산을 처리하는데 왜 다른 명령어로 처리되는지 궁금해서 찾아본 결과,
iconst_<N>은 -1 ~ 5까지의 정수 리터럴만 처리 가능하며, bipush N 은 -128 ~ 127까지의 정수 리터럴을 처리할 수 있다.

iconst_<N>은 특정한 인자가 있는 것이 아니라  iconst_m1, iconst_0, .. , iconst_5와 같이 JVM 내 이미 정의된 명령어를 통해 수행된다.

반면, bipush N은 입력할 정수를 인자(N)에 입력함으로써 처리되고 있다. 결국 N을 저장할 메모리가 추가로 필요하다는 점에서 -1 ~ 5 범위의 있는 정수를 입력할 땐 iconst_N가 사용되는 것이라고 생각된다.

추가적으로 iconst와 bipush에서 i는 integer를 의미하며, 다른 타입에 대한 명령어도 지원하고 있다. 참조

 

 

 

 

https://blog.jamesdbloom.com/JVMInternals.html

'Java' 카테고리의 다른 글

JVM 내부 구조 (1) - 클래스 로더  (1) 2023.11.27
Java 프로그램의 실행 과정  (0) 2023.11.27
JVM, JDK, JRK  (0) 2023.11.27