앞선 👉글에서 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.class
와 Woong.class
두 개의 클래스 파일이 생성될 것이다.javac Main.java Woong.java
생성된 클래스 파일을 HexD
나 Hex 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 Pool
과 Instruction Set
에만 집중하여 JVM을 파악해볼 것이다.
자세한 바이트 코드 정보는 Oracle Doc과 tistory 글에서 확인할 수 있다.
JVM 동작 원리
JVM의 목적 : 바이트코드 형태의 Class 파일을 컴퓨터가 읽을 수 있는 기계어로 번역하여 CPU에 명령을 내림
Java 프로그램이 실행되면 JVM은 먼저 Class Loader
를 통해 Class파일을 읽는다.Class Loader
에 의해 읽혀진 Class파일은 검증과정과 초기화과정(static 변수 초기화 등)을 거쳐 Runtime Data Areas
의 Method Area
라는 메모리 공간에 올려진다.
메모리 공간에 올려진 Class 파일의 바이트코드는 Execution Engine
의 Interpreter
나 JIT Compiler
에 의해 기계어로 번역되어 CPU에 전달된다. 그리고 기계어로 번역될 프로그램 동작과 관련한 정보는 Runtime Data Areas
저장되고, 실시간으로 저장된 정보가 인터프리터에 의해 기계어로 번역되어 CPU로 전달될 것이다.
이제 Runtime Data Areas
의 원리에 집중하면서 JVM의 동작을 살펴볼 것이다.
Runtime Data Areas
Runtime Data Areas
는 크게 다섯가지 공간으로 분류된다.
- Method Area : 클래스에 대한 정보 저장 (스레드 공유 공간)
- Heap : 런타임에 생성되는 모든 객체들의 대한 정보 저장 (스레드 공유 공간)
- JVM Stacks : 메서드를 실행하기 위한 정보들이 저장되는 공간, Frame 자료구조 활용 (스레드당 1개)
- PC Registres : 현재 실행되고 있는 명령의 주소를 저장 (스레드당 1개)
- 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 아키텍처의 각 모듈에 대해 알아보았으니, 이제 동작원리를 알아볼 것이다.
- 클래스로더에 의해 Class 파일이
Method Area
에 올라온다. (스레드가 실행되면서 필요한 클래스들은 필요할 때마다 동적으로 클래스로더에 의해 불려진다.) - 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
에 저장한다.
Instruction Set
의 각 명령어마다 인터프리터에 의해 기계어로 번역되어 실시간으로 실행된다.
클래스 로더에 의해 로드된 Class 파일은 JVM에 의해 위 Flow대로 실행된다.
Garbage Collection
GC(Garbage Collection)는 JVM 메모리를 자동으로 관리해준다. 객체 생명주기에 따라서 자동으로 메모리를 해제시켜주는데,
해당 GC의 원리는 다음 연재될 글에서 살펴보겠다.
'개발 > Java' 카테고리의 다른 글
[Java/모던자바인액션] 람다 표현식과 함수형 인터페이스 (2) (1) | 2024.01.08 |
---|---|
[Java/모던자바인액션] 람다 표현식과 함수형 인터페이스 (1) (0) | 2024.01.02 |
[Java/모던자바인액션] 동작 파라미터화 (1) | 2023.12.16 |
[Java/모던자바인액션] Java 8 등장배경 및 새로운 기능 (0) | 2023.12.04 |
[Java] JVM, JRE, JDK (0) | 2023.07.23 |