-
JVM 및 스택 프레임Develop/Java 2022. 9. 7. 18:48
1. JVM Memory(Runtime Date Areas)
JVM이 운영체제 위에서 실행되면서 할당 받는 메모리 영역(실행 데이터 영역)
- 클래스로더(class Loader)로 부터 해석된 바이트코드가 각 영역에 배치되어 수행
- PC 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)은 스레드(Thread)마다 하나씩 생성
- 힙(Heap), 메서드 영역(Method Area), 런타임 상수 풀(Runtime Constant Pool)은 스레드(Thread)가 공유해서 사용 한다.
2. JVM 스택(JVM Language Stacks)
- 각 스레드 마다 하나씩 존재하며 스레드 시작 시 생성
- 스택 내 데이터는 다른 Thread가 접근이 불가능하여 동기화(Synchronized)에 대한 이슈 발생하지 않음
- 스택 프레임(Stack Frame)이라는 구조체들로 구성되어 있다.
2-1. 스택 프레임(Stack Frame)
- 하나의 스레드(Thread)가 Java Method를 호출 될 때마다 JVM은 Stack Frame을 하나 생성하여 JVM 스택에 생성(Push)
- 여는 중괄호 "{" 를 만날 때마다 스택 프레임(stack frame)이 생성(Push)
- 닫는 중괄호 "}" 를 만나면 스택 프레임(stack frame)이 소멸(Pop)
- 스택 프레임의 크기는 컴파일 타임에 결정됨(Method 내에서 사용하는 변수나 연산에 관련된 내용, 반환 Type등이 이미 Source Code 내에서 결정되기 때문)
- Method가 비정상적으로 종료하게 되어 Exception 처리를 수행한 후 JVM 스택에서 사라짐
- 오류 발생시 printStackTrace()를 통해 Stack Trace로 각 스택 프레임을 출력
- 각 스택 프레임은 로컬 변수 영역(Local Variable Section), 피연산자 스택(Operand Stack), 프레임 데이터(Frame Data)로 구성

1) Local Variable Section
- 메소드 파라미터(Method Parameters), 로컬변수(Local Variable) 저장
- Local Variable Section은 0 부터 시작하는 인덱스를 가진 Array로 구성되어 있으며, 이 Array의 인덱스를 통해 데이터에 접근
- 메소드 파라미터(Method Parameters)는 선언된 순서로 인덱스에 할당
- 로컬변수(Local Variable)는 컴파일러가 알아서 인덱스를 할당
public static int runClassMethod(int i, long l, float f, double d, Object o, byte b) { return 0; } public int runInstanceMethod(char c, double d, short s, boolean b) { return 0; }
- 0번 인덱스 hidden this는 Class의 인스턴스 데이터를 찾아간다. → 클래스 메소드(static으로 선언한 메소드)에는 존재하지 않고 Class자체에 속함(Method Area 영역)
- char, byte, short, boolean형으로 선언한 것들은 모두 int형으로 할당 → boolean 형은 JVM에서 직접 지원하지 않기 때문에 int 형으로 변경하여 저장(boolean형은 하나의 숫자에 불과)하고, char, byte, short는 Local Variable Section이나 Operand Stack에서는 int 형으로 변환되고 다른 곳에서는 원래 형으로 원복하여 저장
- long과 double형은 두 개의 엔트리를 차지 → 8byte(대형 타입)
- Object나, String과 같은 참조형 변수는 reference 형으로 할당 받음(Heap에 할당된 객체의 위치 저장)
- Integer도 마찬가지로 reference값을 통해 Stack에서 Heap으로 넘어가야 하기 때문에 스택에 reference 형으로 저장
- Reference를 통해 객체를 찾아 다니는 작업은 CPU 연산 필요하기 때문에 Integer보다 int와 같은 Primitive Type을 사용하는 것이 성능상 이점
2) Operand Stack
- JVM의 작업 공간 (JVM이 프로그램을 수행하면서 연산을 위해 사용되는 데이터 및 그 결과를 처리하는 공간)
public void operandStack() { int a, b, c; a = 5; b = 6; c = a + b; } }- Byte Code 추출 'Javap -c'
public void operandStack(); Code: 0: iconst_5 1: istore_1 2: bipush 6 4: istore_2 5: iload_1 6: iload_2 7: iadd 8: istore_3 9: return
- 맨앞에 i는 int형을 의미함
- Local Variable Section의 크기는 컴파일 시점에 지정 됨 → 위에서 설명한대로 소스 코드 내에서 결정 되기 때문
- iconst_5는 상수 5를 Stack에 Push 의미
- istore_1은 Local Variable Section의 1번 인덱스 값에 저장 → 상수 5 저장
- bipush 6은 상수 6을 Stack에 Push → istore_2에 저장
- -1보다 작거나 5보다 큰 정수는 각각 Primitive Type의 범위에 따라 bipush, sipush 등의 ByteCode로 표시
- iload_1은 Operand Stack으로 1번 인덱스에서 로드(Push)하라는 의미
- iload_2은 Operand Stack으로 2번 인덱스에서 로드(Push)하라는 의미
- iadd는 int형의 값들을 더하라는 의미 → 5+6 = 11
- istore_3은 Local Variable Section의 3번 인덱스 값에 → 상수 11 저장
- Operand Stack에 잠시 저장된 연산의 결과값이 제거(Pop)
- return은 Method 수행을 마치고 StackFrame을 나감
3) Frame Data
1. Constant Pool Resolution 정보 저장
- Constant Pool의 Pointer 정보
- JVM은 이 Pointer를 이용하여 필요할 때마다 Method Area의 Constant Pool을 찾아감
- Java의 모든 Reference는 Symbolic Reference이기 때문에 Class나 Method, 그리고 변수나 상수에 접근할 때에도 이러한 Resolution이 수행
- Resolution 은 Symbolic Reference가 JVM에서 실제 접근 할 수 있는 실질적인 Direct Reference로 변경하는 것을 의미한다.(Symbolic Reference를 Direct Reference로 변경하는 과정)
- Symbolic Reference는 Method Area의 Constant Pool이라는 곳에 저장되어 있음
- 특정 Object가 특정 Class나 Interface에 의존관계가 있는지 확인하기 위해서도 Constant Pool의 Entry를 참조
public void frameData() { String str = "Hello"; }public void frameData(); Code: 0: ldc #2 // String Hello 2: astore_1 3: return- 🔗ldc → Constant Pool Index를 의미(#2는 Constant_pool 내의 2번째 인덱스에 "Hello" 위치 Pointer)
- 1번째 인덱스는 → Java.Lang.Object을 의미하는 super
invokespecial #1 // Method java/lang/Object."<init>":()V- 맨앞에 a는 reference형 타입을 의미
- astore_1 → Hello의 주소 정보 저장
2. Method가 정상 종료 했을 때의 정보 저장
- 자신을 호출한 Stack Frame의 Instruction Pointer가 들어가 있어서, Method가 종료되면 JVM은 이 정보를 PC Register에 설정하고 Stack Frame을 빠져나감
- 만약 이 Method가 반환 값(return값이 void가 아닌경우)이 있다면 이 반환 값을 다음 번 Current Frame, 즉 자신을 호출한 Method의 Stack Frame의 Operand Stack에 Push하는 작업도 병행
3. 비정상 종료 했을 때 Exception 관련된 정보 저장
- Method가 Exception을 발생시키며 강제로 종료되었을 경우에는 Exception을 핸들링 함
- Frame Data에 Exception 정보 저장하고, 각각의 클래스는 Exception Table를 가지고 있어 Exception이 발생하면 JVM은 이를 참조하여 catch 절에 해당하는 Bytecode로 점프하게 된다.
public void exceptionHandler() { int a, b, c; a = 5; b = 6; try { c = a + b; } catch (NullPointerException e) { c = 0; } } }public void exceptionHandler(); Code: 0: iconst_5 1: istore_1 2: bipush 6 4: istore_2 5: iload_1 6: iload_2 7: iadd 8: istore_3 9: goto 16 12: astore 4 14: iconst_0 15: istore_3 16: return Exception table: from to target type 5 9 12 Class java/lang/NullPointerException }- Exception Table
- from, to, target, type 4가지 요소로 구성
- 5에서 시작 해서 9번 EntryNumber를 의미 (c= a+b 연산 과정)
- exception 발생 시 Exception Object가 type의 클래스 정보와 일치하게 되면 target으로 점프하여 수행
- astore 4 → 인덱스 4번을 의미 3까지는 store_3로 표시
- goto 16 → try 문 안에서 연산 성공 시 return(16)으로 이동
출처