읽기 전
- 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
- 개인적으로 사용해보면서 배운 점을 정리한 글입니다.
문제 제기
변수 저장에 대해 찾아보다 보면 constant pool이라던지 stack이라던지 heap이라던지 용어가 많이 나오는데 constant pool은 method 영역에 속하지만 그 메소드 영역은 어디에 위치하는지 등 diagram으로 그린 글이 몇 없어 이번 기회에 정리해보려 한다. 런타임 데이터 영역에 대해 확실히 정리한 뒤 클래스/인스턴스 변수, 기본/참조 자료형의 저장 위치에 대해 정리해보려 한다.
결정적으로 runtime constant pool과 string constant pool의 개념을 서로 분리해서 설명하는 종자들 때문에 이 포스팅을 작성하게 되었다.
자바 메모리 - Runtime Data Area
런타임 데이터 영역(Runtime Data)은 실제 클래스 파일이 적재되는 곳으로 JVM이 OS로부터 자바 프로그램 실행을 위한 데이터와 명령어를 저장하기 위해 할당받는 메모리 공간이다. 주로 메소드, 힙, 스택 영역을 언급한다.
일반적으로 Runtime Data Area를 정리하면 위의 그림대로 설명하는 편이다.
- 메소드(=클래스=스태틱) 영역
- 가장 먼저 데이터가 저장됨
- 클래스 로더에 의해 로드된 클래스, 메소드 정보와 클래스 변수 정보 저장
- 클래스 변수 남발 시 메모리 공간 부족할 수 있음
- Java 7의 경우 부족할 수 있었으나 Java 8부터는 개선됨
- 프로그램 시작부터 종료될 때까지 메모리에 적재
- 명시적 null 선언 시 GC가 청소
- 모든 스레드가 공유함
- 힙 영역
- 런타임 시 결정되는 참조 자료형이 저장됨
- 런타임 시 결정됨에 따라 동작 중의 문제(범위 초과 참조 등)가 발생할 코드임에도 문법의 문제는 아니기에 컴파일 시 에러를 출력하지 않음
- new 연산자를 통해 생성된 객체(인스턴스)가 저장되는 공간
- 즉, 인스턴스 변수도 여기에 저장됨
- 객체가 더 이상 쓰이지 않거나 명시적 null 선언 시 GC가 청소
- 모든 스레드가 공유함
- 런타임 시 결정되는 참조 자료형이 저장됨
- 스택
- 컴파일 시 결정되는 기본 자료형(&참조변수)이 저장됨
- 컴파일 시 결정됨에 따라 자료형의 범위를 초과한 값 할당 등의 코드가 컴파일 단계에서 검출됨
- 참조 변수는 기본 자료형을 Wrapper Class로 boxing한 변수(Integer, Byte 등)
- 메소드 호출 시 메모리에 FILO로 삽입
- 메소드 종료 시 메모리에서 LIFO로 제거
- 메소드가 호출될 때마다 각각의 스택 프레임이 생성됨
- 각 스택 프레임은 하나의 메소드에 대한 정보를 저장
- {} 혹은 메소드가 종료될 때 삭제됨
- 메소드 종료 시 프레임 별로 삭제
- 각 스레드 별로 생성
- 컴파일 시 결정되는 기본 자료형(&참조변수)이 저장됨
- PC 레지스터
- JVM이 수행할 명령어의 주소를 저장하는공간
- OS의 PC 레지스터와 유사한 역할이나 CPU와는 별개로 JVM이 관리
- 스레드가 시작될 때마다 생성됨
- 각 스레드 별로 생성
- JVM이 수행할 명령어의 주소를 저장하는공간
- 네이티브 메소드 스택
- 바이트 코드가 아닌 기계어로 작성된 코드를 실행하는 공간
- 다른 언어(c/c++)로 작성된 코드를 수행하기 위함
- Java Native Interface를 통해 바이트 코드로 변환됨
- Java Code를 수행하다 JNI 호출 시 Java Stack에서 Native Stack으로 동적 연결(Dynamic Linking)을 통해 확장됨
- 따라서 나뉘어졌다고는 하나 stack에서 연결할 수 있음
- JNI(Java Native Interface) 호출 시 생성
- 각 스레드 별로 생성
Runtime Data Area의 구분
그런데 변수 저장에 대해 공부하다 보면 위의 내용에 따라 클래스 변수는 Method Area에 저장된다는데 오라클 문서에 따르면 logical하게는 heap에 속한다 설명한다. 뭔가 아예 별개로 설명하자니 애매하다. 이에 대해 대부분의 글들은 별다른 설명 없이 넘어가곤 한다. 엄밀하게 각 영역별로 어떻게 구성되는 지 따져보고자 한다. 크게 저장 공간에 따라 heap과 stack으로 구성되고 그 내부에 다른 영역이 포함된다고 간주하면 되겠다.
Heap 영역? Method 영역?
위 그림이 일반적으로 jvm의 heap 구조를 설명할 때 사용하는 그림이다. 그런데 permanent 영역은 보통 heap취급을 하지 않지만 이렇게 heap 영역에 "포함된" 상태다. 그런데 일반적으로 permanent 영역과 heap 영역은 구분해서 설명한다. 그리고 Method 영역은 permanent 영역에 속해있어 heap 영역이 아니라고 보통 이야기한다. 따라서, heap 영역에 포함되어 있기는 하나 heap 영역과는 구분해서 간주함으로 인해 오라클 문서에서 logical하게는 heap의 한 부분이라고 설명하는 것이다.
Heap의 Permanent Generation영역 변화
- Java 7 JVM
<----- Java Heap -----------------> <--- Native Memory --->
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Permanent | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+
<--------->
Permanent Heap
S0: Survivor 0
S1: Survivor 1
- Java 8 JVM
<----- Java Heap -----> <--------- Native Memory --------->
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Metaspace | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+
Permanent 영역은 JVM에 의해 크기가 제한된 영역으로 Java 7까지 유지되었다. 따라서 영역 제한으로 인해 메모리 범위 초과 문제가 있었다. 대신 Java 8부터는 Permanent Generation을 제거하고 Metaspace로 대체하였고 heap이 아니라 JVM에 의해 메모리가 제한되지 않는 Native Memory 영역으로 전환하여 OS에 의해 메모리 할당 공간이 자동으로 조절되므로 이론상 아키텍쳐가 지원하는 메모리 크기까지 확장할 수 있다.
따라서, 애매하게 heap에 걸쳐있던 permanent 영역이 non-heap이라고 구분하던 과거와는 달리 명확하게 method area는 heap이 아니라고 정의할 수 있게 되었다.
변경 이유는 ArrayList와 같은 레퍼런스 타입의 동적 배열 객체를 static으로 생성하면 레퍼런스를 Permanent 영역에 저장하는데 해당 객체 배열에 객체 원소를 추가하면 그대로 static object의 레퍼런스가 Permanent 영역에 쌓일 뿐만 아니라 string literal data를 저장하던 string pool도 permanent 영역에 저장하느라 OOM 에러가 발생하는 이슈가 잦았다고 한다.
Permanent에서 Metaspace로 변경됨에 따른 변화
오라클 문서에 The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap. Hotspot will explicitly allocate and free the native memory for the class meta-data.라고 나와있다. 해석하자면 class meta-data는 native memory로 이동된 Metaspace에 저장하고 permanent에 저장했던 interned strings와 static 변수는 heap 영역으로 보낸다는 의미가 된다. Java8부터는 static 변수를 heap영역에서 관리함은 gc 대상이 될 수 있음을 의미한다. 으레 다들 설명하듯 PermGen에 속한 Method area가 클래스 변수를 저장한다고 알고 있다면 이해하기 쉽지 않다. static 변수는 클래스 변수로 명시적 null 선언이 되지 않으면 gc되어서는 안되는 변수다. Method area가 클래스 변수를 저장한다고 이해하는 시점에서 오해가 발생한다 Method area는 class의 meat-data를 저장할 뿐 실질적인 객체와 데이터는 Method area 바깥의 PermGen에 저장됨을 알아야 한다.
class meta-data가 metaspace로 이동하고 기존에 perment 영역에 저장되던 static object는 heap영역에 저장되도록 변경되었다고 설명하는데 이는 reference는 여전히 metaspace에서 관리됨을 의미하기에 참조를 잃은 static object는 GC의 대상이 될 수 있으나 reference가 살아있다면 GC의 대상이 되지 않음을 의미한다.
따라서, metaspace는 여전히 static object에 대한 reference를 보관하며 애매하게 heap에 걸쳐지지 않고 non-heap(native memory)로 이관되며 static 변수(primitive type, interned string)는 heap 영역으로 옮겨짐에 따라 GC의 대상이 될 수 있게끔 조치한 것이다.
이 내용을 언급하는 이유는 클래스 변수 및 객체의 저장위치와 클래스 메타 정보의 위치가 Method 영역이 속한 ParmGen으로부터 Heap과 메모리로 서로 분리되었다는 점을 의미하기 때문이다.
Stack 영역
PC 레지스터와 스택, Native Method 스택은 각 스레드마다 생성된다고 설명했다. 그리고 스택에는 메소드 호출마다 프레임이 생성되어 쌓이며 프레임에는 리턴할 값, 지역 변수, 연산자 스택, 현재 클래스 constant pool의 값을 호출할 레퍼런스가 있다. 이 레퍼런스를 통해 클래스, 인스턴스 변수들이나 생성된 참조 자료형을 호출한다.
'Development > Java' 카테고리의 다른 글
Java 명령어 실행 시 파일 확장자를 제거하는 이유 (0) | 2021.10.21 |
---|---|
Java float, double, BigDecimal - 부동소수점 오차 (2) | 2021.10.21 |
Java 메소드를 클래스에 넣어야 하는 이유 - .class 파일 구조 (0) | 2021.10.21 |
Java 객체와 변수 데이터의 저장 (0) | 2021.10.21 |
Java 컴파일 및 런타임 환경 구조 (0) | 2021.10.21 |