우아한테크코스 어썸오의 JVM Memory Layout을 듣고 정리한 글입니다.
시작하기 앞서,
나는 [10분 테코톡] 김김의 JVM Specification 을 먼저 들어봤는데 발표와 내용이 너무 깔끔해서 추천한다.
실제 컴퓨터의 메모리 레이아웃은 아래와 같다. 메모리에 OS나 다른 애플리케이션 프로세스들이 상주하고, JVM도 실행이 되면 아래와 같이 공간을 차지하게 된다.
1. JVM의 구조
자바 JVM의 구조는 위와 같다. 그중에 Execuation Engine에 Interpreter, JIT Compiler, Garbage Collector가 있으며, 위 JVM Memory라고 적혀있는 박스를 Runtime Data Areas라고 한다.
자바 코드가 동작하기까지 과정
클래스 파일 안에 어떤 필드가, 어떤 메서드가 있는지, 바이트 코드 등을 포함해서 클래스에 대한 모든 정보가 들어있다. 바이트 코드란 JVM이 이해할 수 있는 명령어 집합을 의미한다.
- 자바 소스코드를 작성하고 컴파일을 하면 .class 파일(자바 바이트코드)이 생성된다.
- 자바 명령어로 바이트코드를 실행한다.
- JVM은 클래스 로더를 통해 클래스 파일을 읽어들인다.
- 클래스 로더는 클래스 파일 정보를 메모리에 올리고 검증하고, 스태틱 변수를 초기화하는 역할을 한다.
- JVM의 목적은 바이트 코드를 기계어로 번역해서 CPU에게 일을 시키는 것이다. 이 핵심적인 일을 Interpreter가 수행하게 된다.
2. Runtime Data Area
Runtime Data Areas는 JVM이 사용하는 메모리 공간이며 5가지 영역으로 구성되어 있다.
3. Method Area & Heap
Method Area와 Heap 영역은 모든 쓰레드가 공유하는 영역이다. 따라서 멀티 쓰레드 프로그래밍을 할 때, 동기화를 주의해야 하는 영역이다.
- Method Area : 클래스에 대한 정보가 저장되는 영역이다. 클래스에 대한 모든 정보가 이곳에 저장된다.
- Heap : 런타임에 생성되는 모든 객체들이 저장되는 영역이다. 가비지 컬렉터가 주로 이곳에서 동작한다.
4. JVM Stack
JVM Stacks(JVM Language Stacks)은 메서드를 실행하기 위한 정보들이 저장되는 공간이다. 프레임이라는 자료구조가 들어간다. 프레임은 메서드가 하나 호출될 때마다 새로 생기고, 메서드가 끝나거나 예외가 터지면 사라지게 된다.
public class Crew {
// TAIL HEAD
// stack = [main] [methodA] [methodB] <- frame
public static void main(String[] args) {
Crew crewObj = new Crew();
crewObj.methodA(3);
}
private int methodA(int param) {
int localVariable = 1;
int sum = localVariable + param;
methodB();
return sum;
}
private void methodB() {
}
}
프레임의 구조는 아래와 같다.
- Current Constant Pool Reference : 현재 클래스의 상수 풀에 대한 참조이다.
- Local Variables Array : 메서드 내부의 지역변수를 담고 있다. 이 값들을 배열에 넣고, 인덱스로 빠르게 접근한다. (인스턴스 메서드는 항상 제일 첫 번째 인덱스에 현재 인스턴스에 대한 참조를 가지고 있다.)
- Operand Stack : JVM은 스택 기반으로 연산을 수행한다. 피 연산 값 혹은 연산의 중간 값들을 저장하기 위한 자료구조로 Operand Stack을 사용한다.
위 methodA는 아래와 같은 바이트코드 흐름대로 실행된다.
만약에 정수가 아니라 객체라면 힙에 저장된 객체의 참조값을 아래와 같이 지역변수 배열에서 사용한다.
0 iconst_1 // 스택에 정수 값 1을 올려라
1 istore_2 // 스택에서 값을 꺼내 2번 인덱스에 정수 값을 저장하라
2 iload_2 // 2번 인덱스의 정수 값을 스택에 올려라
3 iload_1 // 1번 인덱스의 정수 값을 스택에 올려라
4 iadd // 스택 상단의 두 정수 값을 더한 후 스택에 넣어라
5 istore_3 // 스택에 값을 꺼내 3번 인덱스에 정수 값을 저장하라
6 iload_0
7 invokevirtual #14 <Crew.methodB : ()V>
10 iload_3
11 ireturn
5. PC Registers와 Native Method Stacks
PC Registers는 현재 실행되고 있는 명령어의 주소를 저장하고 있는 곳이다. 멀티 쓰레드 환경에서 한 쓰레드가 작업을 하다가 다른 쓰레드로 잠시 CPU 점유를 넘겨주고, 다시 돌아왔을 때 이전에 어떤 명령어를 수행하고 있었는지 기억하고 있어야 이어서 작업을 수행할 수 있다.
Native Method Stacks은 C나 C++로 작성된 메서드를 실행할 때 사용하는 스택이다. JVM Stack, PC Register, Native Method Stack은 쓰레드가 생성될 때마다 같이 생성되고 서로 다른 쓰레드가 침범할 수 없는 영역이다. 우리가 하나의 메서드 안에서 지역변수의 동시성 문제를 걱정하지 않아도 되는 이유는 바로 아래 사진을 보면 쉽게 이해할 수 있다.
6. Constant Pool
이 파트에서는 Constant Pool과 Run-time Constant Pool, 두 용어를 같은 의미로 사용합니다.
Constatnt pool(Run-time Constant Pool)의 정의는 아래와 같다. (JVM, SE19 edition 표준 2.5.5 Run-time Constant Pool)
런타임 상수 풀은 클래스 파일에 있는 constant_pool 테이블의 클래스별 또는 인터페이스별 런타임 표현입니다. 여기에는 컴파일 타임에 알려진 숫자 리터럴부터 런타임에 확인되어야 하는 메서드 및 필드 참조에 이르기까지 다양한 종류의 상수가 포함되어 있습니다. 런타임 상수 풀은 일반적인 기호 테이블보다 더 넓은 범위의 데이터를 포함하지만 기존 프로그래밍 언어의 기호 테이블과 유사한 기능을 제공합니다.
즉, Constatnt pool(Run-time Constant Pool)이란 클래스 내에서 사용하는 상수를 담은 테이블이다. 리터럴에 해당되는 값은 특정 메모리 공간인 Constatnt pool(Run-time Constant Pool)에 있고 필요한 경우 Constatnt pool(Run-time Constant Pool)에서 가져와서 사용한다.
public class Wooteco {
public static void main(String[] args) {
Crew crew = new Crew();
crew.study();
}
}
public class Crew {
private int level = 2;
public void study() {
}
}
javap라는 역 어셈블러를 이용해 Wooteco 클래스 파일의 내부를 볼 수 있다. 내부에는 해당 클래스에 대한 모든 정보가 들어있다. Code라고 적혀있는 부분부터 바이트 코드이다. Constant pool이라고 적혀있는 부분이 존재한다.
$ javap -v Wooteco.class
public class com.example.Wooteco
...
Constant pool:
...
#7 = Class #8
#8 = Utf8 com/example/Crew
#9 = Methodref #7.#3
#10 = Methodref #7.#11
#11 = NameAndType #12:#6
#12 = Utf8 study
{
public com.example.Wooteco();
...
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 //Method java/lang/Object."<init>":()V
#n 은 상수 풀에 대한 참조를 나타낸다. 상수 풀 테이블은 인덱스 1부터 시작한다는 점에 유의해야 한다. 인덱스 값 0은 유효하지 않은 인덱스로 간주된다.
Constant pool의 출력 형식은 index와 type 그리고 value로 이루어져 있다. 7번 인덱스를 보면, 타입이 클래스이고 8번 인덱스를 가리키고 있다. 8번 인덱스에는 com/example/Crew라는 값이 담겨있다. Utf8이라는 타입에서 알 수 있듯이 해당 클래스의 이름, 이걸 fully qualified name라고도 한다. 이를 생 문자열로 담고 있다.
이처럼 참조하는 대상만의 이름을 지칭하는 것을 Symbolic Reference라고 한다. 나중에 이 값이 크루 클래스의 데이터를 가리키는 포인터로 변하는 과정이 존재한다.
(index & type) | (value)
----------------------------------
#7 = Class #8
#8 = Utf8 com/example/Crew
#9 = Methodref #7.#3
#10 = Methodref #7.#11
#11 = NameAndType #12:#6
#12 = Utf8 study
Constant Pool에서에서 제공하는 타입의 종류는 아래와 같다.
- Integer, Float: 32비트 상수 (Boolean, Short, Byte 상수는 Integer 취급)
- Double, Long: 64비트 상수
- String: 실제 바이트를 포함하는 풀의 다른 항목을 가리키는 16비트 문자열 상수
- Class: 정규화된 클래스 이름(fully qualified class name)
- Utf8: 바이트 문자열(a stream of bytes)
- NameAndType: (이름 . 타입)으로 표시. 보통 다른 상수(#13.#15)를 이용하여 이름과 타입을 지정
- Fieldref, Methodref, InterfaceMethodref: (Class:NameAndType)으로 표시. Class와 NameAndType은 위에서 설명한 상수 풀에서 사용되는 타입을 의미함.
load constants
$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment Temurin-17.0.4+8 (build 17.0.4+8)
OpenJDK 64-Bit Server VM Temurin-17.0.4+8 (build 17.0.4+8, mixed mode, sharing)
$ cat Yapp.java
public class Yapp {
public static void main(String[] args) {
float f = 0.1f;
long l = 1000000l;
int i = 1;
short s = 1;
byte b = 1;
double d = 0.1;
}
}
$ javap -v Yapp.class
Classfile /C:/Users/82108/Desktop/swe-2022/java-test/Yapp.class
Last modified 2022. 11. 12.; size 321 bytes
SHA-256 checksum ba5fa5279c9c39e34b5ceb3044dec14402349da8089163515bc5cfcf152d8b7b
Compiled from "Yapp.java"
public class Yapp
minor version: 0
major version: 61
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #12 // Yapp
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 = Float 0.1f
#8 = Long 1000000l
#10 = Double 0.1d
#12 = Class #13 // Yapp
#13 = Utf8 Yapp
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 Yapp.java
{
public Yapp();
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=9, args_size=1
0: ldc #7 // float 0.1f
2: fstore_1
3: ldc2_w #8 // long 1000000l
6: lstore_2
7: iconst_1
8: istore 4
10: iconst_1
11: istore 5
13: iconst_1
14: istore 6
16: ldc2_w #10 // double 0.1d
19: dstore 7
21: return
LineNumberTable:
line 4: 0
line 5: 3
line 7: 7
line 8: 10
line 9: 13
line 10: 16
line 12: 21
}
SourceFile: "Yapp.java"
7. Wooteco 클래스가 실행되기까지 과정
Wooteco 클래스 정보 | |
Constant Pool | |
#3 = Utf8 | Init<> |
#7 = Class | #8 |
#8 = Utf8 | com.example.Crew |
#9 = Methodref | #7.#3 |
Bytecode |
public static void main(java.lang.String[]) |
0: new #7 |
3: dup |
4: invokespecial #9 |
7: astore_1 |
8: aload_1 |
9: invokevirtual #10 |
12: return |
- Wooteco.java 파일을 java 명령어로 실행한다.
- jvm은 classpath를 찾아보면서 Wooteco.class 파일을 읽어온다.
- 해당 클래스 정보를 Method 영역에 올린다.
- JVM은 Method 영역에 저장된 바이트 코드를 해석한다. (main 메서드를 실행)
- JVM은 Main 메서드를 실행하면서 Constant pool에 대한 포인터를 하나 유지한다. (Constant pool을 언제든지 직접 참조할 수 있도록 계속 쳐다본다.)
- 이때까지 아직, 크루 클래스가 Method 영역에 올라오지 않는다. (대부분의 구현체는 해당 클래스가 필요할 때 클래스 정보를 동적 로딩한다.)
- 0번 명령어는 JVM에게 Constant Pool에 있는 클래스를 위한 메모리 공간을 할당할 것을 명령한다.
- Constant Pool #7에 있는 값은 #8을 가리키고 있고, #8은 com.example.Crew라는 문자열이 저장되어 있다.
- com.example.Crew 는 Symbolic Reference이다. 아직은 Crew 클래스가 Method 영역에 올라오지 않았다면 클래스 로더에게 요청을 보내 , 메모리에 로드하도록 한다.
- 이후에는 Wooteco/Constant Pool/#8의 값인 Symbolic Reference를 크루 클래스 데이터를 직접 가리키는 참조로 바꾼다. 이를 Constsant Pool Resolution이라고 한다. (run-time constant pool에 있는 symbolic references들로부터 변수의 값을 결정하는 과정)
- JVM은 크루라는 객체를 할당하는데 얼마만큼의 힙 공간이 필요한지 알아내기 위해서 다시 한번 우테코 클래스의 Constant Pool을 바라본다.
- JVM은 항상 메서드 영역의 저장된 클래스 정보를 보고, 객체를 표현하는데 필요한 메모리 크기를 결정할 수 있다. 이에 필요한 모든 정보가 Method 영역에 있기 때문이다.
- JVM이 객체를 위해 필요한 힙 영역의 크기를 결정하고 나면, 힙 공간을 할당하고 인스턴스 변수 값을 초기값으로 초기화한다. main메서드의 첫 번째 명령어는 새로운 크루의 객체의 참조를 스택에 푸시하면서 끝이 나게 된다.
- 다음 명령에서 study 메서드를 호출할 때, 이 참조값을 사용하게 된다.
'맛있지만 저작권 문제 > 테코톡' 카테고리의 다른 글
[테코톡 정리] 제리의 MVC 패턴 (0) | 2022.11.12 |
---|---|
[테코톡 정리] 오리와 코린의 Merge, Rebase, Cherry pick (1) | 2022.11.05 |