🚀 JVM의 Runtime Data Area의 5가지 구성에 대해서 알아봅니다.
🚀 각 구성의 특징을 이해합니다.
💡 자바는 "Write Once, Run Everywhere"라는 철학으로 만들어진 프로그래밍 언어입니다.
💡 즉, 플랫폼 독립성을 목표로한 언어입니다.
💡 자바에서는 이를 4가지 연관 기술을 통해 실현합니다.
- JAVA Programming Language
- JAVA Class File Format
- Java Application Programming Interface(Java API)
- Java Virtual Machine(JVM)
이 문서에서는 Java Virtual Machine, 그 안에서도 Runtime Data Area을 살펴보겠습니다.
-Instance, 배열이 저장되는 공간입니다.
👉 Eden : Object가 최초로 할당되는 영역입니다.
해당 영역이 꽉차면 Manor GC가 발생하는데 이때 살아있는 객체는 Survivor1영역으로 이동합니다.
죽은 객체는 남겨놨다가 모든 살아있는 객체가 Survivor1영역으로 이동하면 Eden영역을 비웁니다.
👉 Survivor1 & 2 : Eden에서 살아남은 객체가 머무는 영역입니다.
살아있는 객체를 옴길 때는 반드시 한 Survivor영역은 비워줘야합니다.
👉 Youn Generation에서 오래 살아남은 객체가 이동되는 곳 입니다.
이렇게 이동하는 것을 promotion이라 합니다.
오래 참조될 가능 성이 높은 객체를 모아두는 장소입니다.
* 메모리 구조 외에 GC의 자세한 메커니즘은 Garbage Callection파트에서 다루겠습니다.
load된 class는 아래 7개의 정보로 구성됩니다.
타입(클래스나 인터페이스)의 전반적인 정보를 담습니다.
: class나 interface에 대한 모든 상수 정보를 가지고 있습니다.
이때 상수란 literal constant를 포함한 type, field, method로의 모든 참조를 말합니다.
하나의 클래스마다 하나의 상수 풀을 가집니다
상수풀은 1~n으로 넘버링 되어있으며 타입, 값에 대한 정보를 담고 있습니다.
아래 바이트 코드 참고해봅시다.
public class org.jvminternals.SimpleClass
SourceFile: "SimpleClass.java"
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
//상수풀!!! 이 부분을 보세요!
Constant pool:
#1 = Methodref #6.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // "Hello"
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // org/jvminternals/SimpleClass
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lorg/jvminternals/SimpleClass;
#14 = Utf8 sayHello
#15 = Utf8 SourceFile
#16 = Utf8 SimpleClass.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 Hello
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 org/jvminternals/SimpleClass
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
{
public org.jvminternals.SimpleClass();
Signature: ()V
flags: 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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/jvminternals/SimpleClass;
public void sayHello();
Signature: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String "Hello"
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lorg/jvminternals/SimpleClass;
}
위의 sayHello 메소드의 바이트코드 3번 라인을 보면 ldc #3 라는 Instruction이 있습니다.
이는 상수풀의 3번째 데이터를 로드 하라는 의미고 옆에 그 데이터가 hello라는 String 데이터라 주석을 달아줍니다.
위로 올라가 상수풀을 직접보면 해당 데이터가 무엇인지 알 수 있습니다.
: class에서 선언된 모든 field 정보가 있습니다.
: class에서 선언된 method의 정보가 저장되는 영역입니다.
: class 안에서 static 으로 선언된 변수가 저장 되는 영역입니다.
: class가 JVM에 의해 load될 때 어떤 Class Loader에 의해서 load되었는지 정보가 저장 되어있습니다.
: class가 JVM에 의해서 load되면 항상 java.lang.class 클래스의 instance가 생성됩니다.
: Thread에서 method를 수행 할 때 수행정보를 저장하는 공간입니다.
이 수행 정보는 Local Variable Section, Operand Stack, Frame Data로 구성됩니다.
- Thread가 Method를 수행하면 Stack Frame을 생성하여 JVM Stack에 push 한다.
- 컴파일 타임에 사이즈가 정해진다.
- 새로 들어간 Stack Frame이 Current Frame이 된다.
- 해당 Method가 수행을 마치면 Current Frame은 pop된다.
- pop 이후 이전 Stack Frame이 Current Frame이 된다.
- Method 수행 중 Exception이 발생해도 Stack Frame에서 pop된다.
: 수행하는 Method의 Parameter Variable, Local Variable들을 저장하는 공간입니다.
- Local Variable Section은 배열이다. 0부터 할당되며 index로 접근한다
- instance method는 0에 this를 할당하고, static method는 0부터 Parameter Variable을 할당한다.
- Parameter Variable 순서대로 할당되며, Local Variable은 컴파일러가 알아서 할당한다
- Local Variable은 컴파일러가 최적화 하여 할당 할수도 안할 수도 있다
- Local Variable Section 컴파일 타임 Stack Frame이 사이즈가 정해지면서 같이 사이즈가 정해진다.
- 각 인덱스의 크기는 32bit이다.
- primetive 타입 중 long과 double은 두 칸을 차지하고 나머지는 1칸씩 차지한다
- byte, char, short Stack Frame 에는 int형으로 변환하여 저장되고, Heap Area에서는 원래형으로 복원된다.
- boolean도 Stack Frame에서 int 형으로 변환되지만, Heap Area에서 다시 원래 타입으로 복원되지 않는다.
- long과 double은 64bit의 크기를 가지며 Local Variable Section에서 두 칸을 차지하는데,
두 자료형에 접근할 때는 첫번째 칸의 인덱스로 접근한다.
- reference 타입은 주소값이 할당되며 1칸을 차지한다.
:피연산자 스택, JVM이 Local Variable Section에서 데이터를 가져와 계산할 때 사용하는 작업공간입니다.
👉 특징
- Stack 자료구조를 갖는다.
- Local Variable Section에서 값을 받아오거나, 연산한 결과를 돌려주는 역할을 한다.
👉 연산 방식
- Instruction을 실행하기 위해서 [[Local Variable Section]]에서 값을 복사하여 Operand Stack에 push한다.
- push된 값을 모두 pop하여 연산을 한다.
- 연산 후 Operand Stack에 push한다.
- push된 연산결과를 pop하여 [[Local Variable Section]]에 넣는다.
: Stack Frame의 정보를 저장하는 영역입니다.
👉 구성
- Constant Pool Resolution 정보(상수풀에 대한 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경)
- Method 정상 종료 정보(자신 스택프레임을 호출한 상위 스택프레임이 어딘지)
- Method 비정상 종료 시 Exception 관련 정보(예외 발생 시 처리해야할 정보)
👉 특징
- Constant Pool Resolution을 통한 Contant Pool 접근
- Stack Frame은 method가 실행되면서 생성됩니다.
- 현재 실행 중인 method의 정보를 확인할 수 있고, 해당 method가 속한 class의 상수풀을 참조할 수 있습니다.
- Constant Pool Resolution은 현재 클래스의 Contant Pool에 접근할 수 있는 포인터 정보입니다.- 자신을 호출한 Stack Frame의 Instruction Pointer를 가지고있습니다.
- Method가 종료되면 JVM은 이 정보를 PC Register에 설정하고 Stack Frame을 빠져나옵다.
- 만약 return 값이 있으면 자신을 소출한 method의 Stack Frame의 Operand Stack에 push합니다.- Method의 비정상 종료에 대한 정보를 담고 있습니다.
- Exception Table의 Reference를 가지고 있습니다.
- 각 class 파일은 Exception Table을 가집니다.
- Exception이 발생하면 jvm은 Exception Table 참조하여 catch절에 해당하는 bytecode로 점프합니다.
Exception Table
from | to | target | type |
---|---|---|---|
5 | 9 | 12 | class java/lang/NullPointException |
- from은 try 절이 시작되는 bytecode의 라인 넘버를 의미합니다
- to는 try 절이 끝나는 bytecode의 라인 넘버를 의미합니다
- target은 try절에서 Exception이 발생 했을 때 점프해야할 bytecode의 라인 넘버를 의미합니다
- type은 catch에서 정의한 exception을 의미합니다
- exception이 발생하면 type의 클래스 정보와 비교하여 target으로 점프할지 여부를 정함.
비일치 시 Current Frame을 종료 후 이를 호출한 Method의 Stack Frame에 Exception을 다시 던져 위 과정을 반복합니다다.
- Java metod 1를 호출하면 StackFrame 1 push.
- StackFrame 1에서 Native Function을 호출하면 StackFrame 2가 push됨
- 그리고 Native Method Stack에도 Stack Frame이 push됨
- Native Method Stack의 Stack Frame이 작업을 수행함
- Native Function의 작업이 끝나면 Thread는 JVM Stack에 StackFrame 3 을 새로 생성하고 이곳으로 돌아옴. (Native Function을 호출했던 StackFrame 2로 돌아가지 않음)
참조:
Java Performance Fundamental - 김한도 저