Runtime Data Area
Runtime Data Area는 JVM의 메모리 구조를 의미하며 총 5가지 영역으로 구분됩니다.
- Thread가 모두 공유하는 영역
- Method Area
- Heap
- Thread가 독립적으로 사용하는 영역
- Stack
- PC Register
- Native Method Stack
Method Area
java 파일은 Java 컴파일러에 의해 JVM에서 읽을 수 있도록 Java Bytecode로 변환하여 class 파일을 생성합니다.
이렇게 생성된 class 파일은 JVM의 Class Loader에 의해서 JVM 메모리 영역에 클래스 정보를 올리게 되는데
이때 클래스 정보가 올려지는 곳이 Method Area입니다.
Method Area는 크게 아래와 같은 정보를 담고 있습니다.
- 클래스 구조 정보
- 클래스명, 패키지명, 부모 클래스 정보 등
- Runtime Constant Pool
- 리터럴, 심볼 참조(메서드, 필드, 클래스)
- 필드 정보
- 메서드 정보
- 메서드의 바이트코드
- static 변수
아래 java 코드를 컴파일하여 Method Area 영역에 올라가는 예시를 보겠습니다.
public class Example {
public static final String GREETING = "Hello, JVM!";
private int number;
public Example(int number) {
this.number = number;
}
public void printGreeting() {
System.out.println(GREETING + " Your number is: " + number);
}
public static void main(String[] args) {
Example ex = new Example(42);
ex.printGreeting();
}
}
Example.java 코드를 실행시키게 되면 위와 같이 Method Area 영역에 클래스 정보가 올라가게 됩니다.
Main 메서드 실행시 Thread가 생성되며 해당 Thread는 Method Area에서 Example 클래스 정보를 참조하여 프로그램을 실행시키게 됩니다.
The Java Virtual Machine Specification(2.5.4) 에서는 Method Area를 Heap의 한 부분으로 기술하지만 Heap과 구분하기 위해 Non-Heap이라고 부르기도 합니다.
JVM 명세에서는 특정 방식을 강제하지 않아 벤더사가 구현 방식을 정하면 되었습니다.
Hotspot VM 개발팀은 Permanent Generation을 구현하여 GC의 수집 범위를 Method Area까지 확장하기로 결정했고 Java 7 이전에는 Method Area를 Permanent Generation에 구현하였습니다.
(참고로 다른 VM은 Permanent Generation라는 개념이 없었습니다. ex-BEA JRockit, IBM j9)
하지만 Permanent Generation은 JVM 시작시 크기가 고정되어 클래스가 많이 로드되면 OOM이 발생하였습니다.
OutOfMemoryError: PermGen space
Java 7에서는 이와 같은 문제를 개선하고자 Permanent Generation(Method Area)에서 관리하던 문자열 상수와 정적 변수등의 정보를 Heap으로 옮겼고, Java 8부터는 Permanent Generation 개념을 지우고 Permanent Generation에 남아있던 모든 정보를 네이티브 메모리에 구현한 Metaspace로 옮겼습니다.
네이티브 메모리에 구현함으로써 OS 메모리만큼 자동으로 확장이 가능하기에 크기가 고정되었던 문제를 해결할 수 있었습니다.
Heap
Heap은 인스턴스 객체와 배열이 저장되는 영역으로 GC의 대상이 되는 영역입니다.
Method Area와 마찬가지로 모든 Thread가 공유하는 영역으로 동시성 이슈가 발생할 수 있습니다.
Heap의 생성된 인스턴스의 참조값은 Stack, Native Method Stack, 상수 풀등에 저장됩니다. (Root Space)
Heap 영역이 가득차게 되면 OutOfMemoryError가 발생하게 됩니다.
상기에 기술하였듯이 JVM의 상세한 구현은 벤더들에게 일임되어 있어 GC뿐만 아니라 Heap의 전반적인 구성도 특별히 정의된바 없이 JVM을 구현하는 벤더에게 전권을 위임하고 있습니다.
(대체로 Hotspot VM이 많이 사용되고 있습니다.)
GC는 세대 단위 컬렉션 이론에 기초해 설계되었습니다.
약한 세대 가설 : 대다수 객체는 일찍 죽는다.
강한 세대 가설 : 가비지 컬렉션 과정에서 살아남은 횟수가 늘어날수록 더 오래 살 가능성이 커진다.
세대 간 참조 가설 : 세대 간 참조의 개수는 같은 세대 안에서의 참조보다 훨씬 적다.
위 이론에 기반하여 Heap 영역을 나누게 되었습니다.
- Young Generation
- 새로 생성된 객체가 저장되는 공간
- GC가 가장 자주 발생하는 영역(Minor GC)
- Eden
- 객체가 처음 생성될 때 저장되는 공간
- 대부분의 객체가 짧은 생명주기를 가지므로 빠르게 GC 대상이 됩니다.
- Eden 영역이 꽉 차게 되면 Minor GC가 실행되어 참조가 살아있는 객체는 Survivor(S0 or S1) 영역으로 이동하게 되고 남은 객체는 모두 제거하게 됩니다.
- Survivor
- Eden 영역에서 살아남은 객체가 이동하는 공간
- 한 Survivor 영역이 가득 차면 다른 Survivor 영역으로 복사하고 이전 Survivor는 비우게 됩니다.
- 일정 횟수 이상 참조되어 기준 Age를 넘기면 Old Generation으로 이동하게 됩니다.(Promotion)
- Survivor 영역 중 하나는 반드시 비어 있어야 함
- Old Generation
- Young Gen에서 살아남은 객체들이 이동하는 공간
- 객체 크기가 크거나 Survivor 영역의 메모리가 부족할 경우 Eden 영역에서 바로 이동할 수도 있습니다.
- 객체의 생명주기가 긴 경우 해당 영역으로 이동하게 됩니다.
- Old Gen에 객체가 가득 차게 되면 Full GC가 실행됩니다.
- Full GC가 실행하게 되면 GC를 수행하는 Thread를 제외한 모든 Thread가 멈추게 됩니다. -> 이를 stop-the-world라고 합니다.
- Full GC는 Minor GC보다 느리며 시스템 성능에 영향을 줍니다.
- Old Gen에는 카드 테이블이 존재합니다.
- Old Gen에 있는 객체가 Young Gen의 객체를 참조할 때마다 정보가 표시되어 Young Gen의 Minor GC가 실행될 때 Old Gen에 있는 모든 객체의 참조를 확인하지 않고 카드 테이블만 확인하여 GC대상을 식별합니다.
- Young Gen에서 살아남은 객체들이 이동하는 공간
하지만 위 영역은 G1 GC가 등장하면서 Region이라는 단위로 나뉘어지게 됩니다.
각 Region 별로 Eden, Survivor, Old등 동적으로 세대를 관리합니다.
Stack
지역변수, 메소드의 매개변수, 리턴 값 등 임시적으로 사용되는 값들이 저장되는 영역으로 PC Register, Native Method Stack과 함께 Thread가 생성될 때마다 독립적으로 할당됩니다.
Stack은 메서드 호출시 생성되는 프레임과 지역 변수 및 메서드 호출에 필요한 정보를 저장하는데 사용 됩니다.
Thread가 메서드 호출시 프레임이 Push되고 메서드 실행이 끝나면 Pop이 됩니다.
메서드 호출이 너무 깊거나 무한 재귀 호출이 일어날 경우 스택의 크기를 초과하여 StackOverFlowError가 발생할 수 있고 Thread 생성시 Stack에 할당할 메모리가 부족하게 되면 OOM이 발생할 수 있습니다.
(해당 내용 관련해서 실제로 OOM이 발생했던 경험을 포스팅한 적이 있으니 참고해주세요! - 링크)
Stack 프레임의 구조는 위 그림을 참고해주세요
- Local Variable Array
- 메서드 내에서 선언된 변수나 파라미터 정보를 해당 영역에 저장합니다.
- 지역 변수는 기본 타입의 값(상수 풀)이나 객체에 대한 Refernece(Heap)가 될 수 있습니다.
- Operand Stack
- 연산을 위한 임시 공간으로 계산을 할때 필요할 수 있는 값들이 임시로 저장됩니다.
- 메서드 실행 도중 연산을 위해 사용됩니다.
- Current Class Constant Pool Reference
- Method Area의 Runtime Constant Pool에 대한 참조입니다.
- 메서드가 속한 클래스의 상수를 사용하기 위해 RCP에 대한 참조값을 가지게 됩니다.
PC Register
Stack과 Native Method Stack과 함께 Thread가 생성될 때 각 Thread마다 할당되는 영역으로 현재 수행중인 JVM의 명령어 주소를 저장합니다.
현재 실행중인 Thread의 바이트코드 줄 번호 표시기라고 생각하면 쉽습니다.
JVM의 명령어 해석(인터프리팅) 과정에서 필수적인 역할을 합니다.
각 Thread마다 독립적인 PC Register를 유지하여 멀티스레드 환경에서도 독립적으로 명령어를 실행시킬 수 있습니다.
Thread가 컨텍스트 스위칭될 때 해당 Thread의 PC Register 값도 저장되고 복원됩니다.
아래 그림을 참고하면 PC Register가 어떤 역할을 하는지 이해할 수 있습니다.
Native Method Stack
JVM이 Java 프로그램에서 네이티브 메서드를 실행할 때 사용하는 스택 영역으로 C, C++ 등으로 작성된 네이티브 코드를 실행할 때 필요하며 JNI(Java Native Interface)를 통해 네이티브 코드를 실행합니다.
JVM은 내부에 많은 OS의 네티이브 라이브러리를 사용하여 구현되는데 JVM 자체적으로 실행할 수 없으며 OS의 네이티브 API를 호출해야 합니다. 이때 Native Method Stack이 필요합니다.
파일 시스템 접근(File I/O), 네트워크 통신(Sockets, HTTP requests), Thread 관리, 메모리 관리 등등
Java 메서드를 실행할 때는 PC Register가 바이트코드의 주소를 저장하지만 Native 메서드가 실행될 때는 PC Register의 값이 특정한 주소를 가리키지 않으며 Undefined 상태가 되고 이에 대한 처리는 Native Method Stack에서 담당합니다.
Stack 영역과 마찬가지로 크기를 초과하면 StackOverflowError가 발생할 수 있습니다.
참고
- https://velog.io/@impala/JAVA-JVM-Runtime-Data-Area
- https://youtu.be/GU254H0N93Y?feature=shared
- https://dzone.com/articles/java-memory-architecture-model-garbage-collection
- JVM 밑바닥까지 파헤치기
Java Memory Architecture Cheat Sheet
dzone.com
JVM 밑바닥까지 파헤치기 | 저우즈밍 - 교보문고
JVM 밑바닥까지 파헤치기 | 자바 가상 머신의 깊숙한 내부를 향해 떠나는 흥미진진한 모험C·C++를 사용해 주로 프로그래밍을 하던 시절 까다로운 메모리 관리와 플랫폼 이식성 문제는 개발자들에
product.kyobobook.co.kr
'Study > Java' 카테고리의 다른 글
[Java] Java에서 Hash를 많이 사용하는 이유 (0) | 2025.04.09 |
---|