런타임 데이터 영역(Runtime Data Area)은 실제 클래스 파일이 적재되는 곳으로 JVM이 OS로부터 자바 프로그램 실행을 위한 데이터와 명령어를 저장하기 위해 할당받는 메모리 공간이다. 런타임 데이터 영역은 크게 다섯 가지 영역으로 나뉜다.
- 메서드 영역 (Method Area)
- 힙 영역 (Heap Area)
- 스택 영역 (Stack Area)
- PC 레지스터 (Program Counter Register)
- 네이티브 메서드 스택 (Native Method Stack)
그리고 Java에서 Thread가 공유하는 영역과 공유하지 않는 영역은 다음과 같다.
🫧 Thread가 공유하는 영역
- 힙 영역
- 메서드 영역
🫧 Thread가 공유하지 않는 영역
- 스택 영역
- PC 레지스터 영역
- 네이티브 메서드 스택
🌱 PC 레지스터 (Program Counter Register)
스레드는 스레드마다 각자의 메서드를 실행하는데, 스레드 별로 멀티스레드 환경이 보장되어야 하므로 명령어 주소값을 저장할 공간이 필요하다. 따라서 PC 레지스터 영역에 각 스레드마다 현재 수행 중인 JVM 명령의 주소를 저장하고 관리하여 추적이 가능하게 만들어준다.
이는 OS의 PC 레지스터와 유사한 역할이나, CPU와는 별개로 JVM에서 관리한다.
만약 실행했던 메서드가 native 하다면, 해당 명령어의 위치를 알 수 없기 때문에 PC 레지스터에 undefined 값을 기록한다. 실행했던 메서드가 native 하지 않다면, PC 레지스터는 JVM에서 사용된 명령의 주소값을 저장한다.
🤓 native 하다는 것은?
Java가 아닌 다른 언어(C/C++)로 실행된 메서드를 의미한다.
🌱 네이티브 메서드 스택 (Native Method Stack)
위에서 PC 레지스터에 native 메서드들은 PC 레지스터에 저장이 되지 않는다고 설명했다. 이는 네이티브 메서드들을 위한 영역이 별도로 필요하다는 것을 알 수 있다. 네이티브 메서드 스택 영역이 이 네이티브 메서드들을 실행하기 위한 스택 영역이다.
네이티브 메서드 스택 영역 또한 스택 영역과 마찬가지로 각 스레드마다 개별적으로 생성되며, 자바 외부의 네이티브 코드를 호출할 때마다 생성되었다가 호출 완료 시 소멸된다.
🌱 스택 영역 (Stack Area)
스택 영역은 지역 변수, 매개변수, 메서드 리턴 값, 연산에 사용되는 임시 데이터 등이 저장되는 영역이다. 스택에 저장되는 데이터들은 프레임 구조로 저장된다. 또, 스택 영역은 각 스레드마다 개별적으로 생성되며, 메서드 호출 시 생성되었다가 메서드 종료 시 소멸된다.
🌱 힙 영역 (Heap Area)
런타임에 결정되는 참조 자료형과 new 연산자를 통해 생성된 객체(인스턴스)가 힙 영역에 저장되며 모든 스레드가 공유한다. 객체가 더 이상 사용되지 않거나 명시적으로 null로 선언되면, GC가 해당 객체를 청소한다.
🌱 메서드 영역 (Method Area) 혹은 Perm Gen
메서드 영역은 모든 스레드가 공유하는 영역으로, JVM이 시작될 때 생성되어 클래스 로더에 의해 로드된 클래스 정보를 저장한다. 이 영역에는 각 클래스의 바이트 코드, 상수, 필드, 메서드 등 클래스 관련 정보가 저장된다.
메서드 영역은 프로그램 시작부터 종료까지 메모리에 적재된 상태를 유지하며, 해당 영역에서 더 이상 데이터를 저장할 공간이 없을 경우 OutOfMemoryError가 발생한다.(Java 8부터 개선됨)
✅ Permanent Generation은 메서드 영역의 구현체!
메서드 영역은 JVM 벤더마다 다르게 구현되어 있다. 그중 Oracle Hotspot JVM JDK 7까지는 메서드 영역을 Permanent Generation(PermGen)이라고 불리기 때문에 메서드 영역과 혼용되어 쓰인다.
이 Perm Gen은 JDK 8부터는 Metaspace로 완전히 대체되었다.
🤔 메서드 영역의 메모리 관리
자바는 동적 로딩을 활용하기 위해 컴파일 시점에 로딩되는 클래스가 있고, 런타임 시점 혹은 로드 타임에 로딩되는 클래스가 있기 때문에 메서드 영역에 애플리케이션에서 사용되는 모든 클래스의 메타 데이터가 저장되는 것은 아니다.
따라서 메서드 영역에 저장되어 있는 클래스의 메타 데이터가 힙 영역에 있는 객체들과 연결되어 있지 않은 데이터라면(GC가 힙 영역에서 객체의 메모리를 회수하고 해당 클래스의 인스턴스가 Heap 영역에 없을 때) 메서드 영역에 있는 클래스의 메타 데이터가 삭제된다.
다만 공식 문서에 따르면, JVM 벤더에 따라 메서드 영역에서 GC가 동작할 수도 있고 하지 않을 수도 있다. 즉 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.
🤔 왜 Permanent Generation이 Meataspace로 대체된 거야?
JDK 7 메모리 구조 (PermGen)
JDK 8 메모리 구조 (Metaspace)
Perm Gen 영역이 JDK 8부터는 Metaspace로 완전히 대체되었다.
기존 Perm Gen은 JVM에 의해 크기가 제한된 영역으로 유지되었다. 따라서 영역 제한으로 인한 메모리 범위 초과(OOM: OutOfMemoryError) 문제가 있었다. 또, 크기가 작아 비용이 많이 드는 가비지 컬렉션을 수시로 수행해 필요한 공간을 확보해야 했고, 클래스 로더들이 제대로 GC 되지 않는 문제로 인해 메모리 누수가 발생하곤 했다.
static int i = 1; // 1. primitive 타입으로 선언된 static field
static void a(){} // 2. static method
static method static A a = new A(); //3. reference 형식의 static field (static object)
static은 위와 같은 방식으로 사용될 수 있는데, primitive 타입의 static variables method, reference 형식의 Object는 permanent 영역에 저장된다.
static A a = new ArrayList<A>();
a.add(new A());
a.add(new A());
a.add(new A());
a는 static으로 선언되어 있기 때문에 a 참조 변수는 메서드 영역인 Perm Gen에 저장되고 ArrayList<A> 객체와 내부 요소들은 힙 영역에 저장된다. 이처럼 static 필드의 참조 변수는 Perm Gen에 저장되므로, 클래스나 필드가 많아지면 Perm Gen이 가득 차 OOM(Out Of Memory)가 발생할 수 있다.
또한, Perm Gen 영역에는 String 리터럴 데이터를 저장하는 String pool도 포함되어 있어, 이로 인해 OOM(Out Of Memory) 에러가 발생할 수 있다.
따라서 Perm Gen이 Metaspace라는 영역으로 대체되면서, 기존 Java Heap 메모리와는 별도로 OS에서 제공하는 네이티브 메모리를 사용하게 되었으며, 필요에 따라 자동으로 크기를 조정하여 공간을 확보할 수 있게 되었다.
이러한 변경사항으로 클래스 메타데이터 사용량이 Metaspace의 최대 크기에 도달할 때 자동으로 GC가 돌게 되면서 JVM은 OutOfMemoryError의 발생확률을 줄일 수 있다.
Java 8 관련 Oracle 문서에서 interned Strings와 클래스 정적 변수들을 Heap 영역으로 보낸다는 내용을 볼 수 있다.
* interned String은 String Constant Pool에서 관리하는 String constant를 말한다.
String apple = "사과"; // interned String = String Constant Pool에 저장되고 재사용 됨
String banana = new String("banana"); // heap에 생성됨
Class metadata, interned Strings and class static variables will be moved from the permanent generation to either the Java heap or native memory.
The code for the permanent generation in the Hotspot JVM will be removed.
Hotspot JVM에서 permanent generation은 제거되고, perm gen에서 관리하면 클래스 메타데이터는 Native Memory(OS에서 제공하는 메모리) 영역으로 옮겨지고 interned String, 클래스 정적 변수는 heap 영역으로 옮겨진다.
The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap.
Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.
클래스 메타 데이터를 Native Memory 영역에 할당하고, interned Strings와 클래스 정적 변수들을 Heap 영역으로 보낸다.
따라서 새로운 클래스 메타 데이터는 기존의 Permanent 영역처럼 고정된 크기가 아닌 사용 가능한 Native Memory 의 크기에 따라 동적으로 할당된다.
> Java 8부터 Perm Gen이 제거되면서, 클래스 메타데이터는 Native Memory로 할당되고 , interned Strings와 클래스 정적 변수는 Java Heap으로 이동하였다. 이로 인해 클래스 메타데이터의 할당은 고정된 크기 제한이 아닌 사용 가능한 Native Memory의 크기에 따라 동적으로 조정되며, interned Strings와 클래스 정적 변수는 Java Heap의 크기 제한에 따라 동적으로 조정된다.
또, 오라클 버그 레포트에서도 관련 업데이트를 찾을 수 있다.
JDK8 레퍼런스에 나온대로 "클래스 정적 변수(static 변수)를 heap으로 보낸다"는 것은 GC의 대상이 될 수 있다는 것을 의미하는데, static 변수는 클래스 변수이기 때문에 명시적 null 선언이 되지 않으면 GC 되어서는 안 되는 변수이다.
❗️ 클래스 변수가 GC 대상이 된다고??
공식 문서에서 static 변수(primitive type, interned string)를 힙 영역으로 보낸다고 했지만 이는 해당 변수의 참조는 Metaspace 영역에 저장되고 실질적인 객체와 데이터를 힙 영역에 저장한다는 것이다. 따라서 참조를 잃은 static 변수들만 GC의 대상이 될 수 있다.
런타임 상수 풀 (Runtime Constant Pool)
런타임 상수 풀은 클래스 로더가 메서드 영역에 클래스를 로딩할 때, 같이 메서드 영역에 적재된다. 즉, 클래스 별로 각각 따로 런타임 상수 풀을 가지고, 해당 클래스의 상수들이 이 풀에 저장된다. 런타임 상수 풀에는 클래스 및 인터페이스의 상수뿐 아니라 메서드와 필드에 대한 모든 레퍼런스 정보를 갖고 있다.
🤔 상수 풀이라는 것도 있던데 런타임 상수 풀이랑 다른 건가?
상수 풀(Constant Pool)과 런타임 상수 풀(Runtime Constant Pool)은 서로 관련은 있지만, 약간 다른 개념이다.
자바는 이진 형식의 클래스 파일로 컴파일된 프로그램을 저장하는 데, 클래스의 메타데이터와 코드를 저장하는 데 사용된다. 클래스 파일은 여러 섹션으로 나뉘는데, 그중 하나가 Constant Pool 테이블이다.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello!!");
}
}
Hello.class:
Magic: 0xCAFEBABE
Minor Version: 0x0000
Major Version: 0x0034
Constant Pool Count: 0x000a
Constant Pool:
#1 = Utf8 "Hello"
#2 = Class #1
#3 = Utf8 "java/lang/Object"
#4 = Utf8 "main"
#5 = Utf8 "([Ljava/lang/String;)V"
#6 = Utf8 "args"
#7 = Utf8 "[Ljava/lang/String;"
#8 = Utf8 "System"
#9 = Utf8 "out"
#10 = Utf8 "Ljava/io/PrintStream;"
위 클래스를 컴파일한 후 클래스 파일을 열어보면 상수 풀이 다음과 같이 저장되어 있는 것을 볼 수 있다.
이처럼 상수 풀은 클래스 파일 내에서 상수들을 저장하는 테이블로, 클래스의 메타데이터, 리터럴 값, 메서드 및 필드에 대한 참조 등을 포함한다. 이 테이블은 클래스 파일의 로딩 과정에서 읽히고 해석된다.
한편, 런타임 상수 풀은 JVM이 클래스를 로드하고 실행하는 동안 메모리에 생성되는 상수 풀을 의미한다. 클래스 파일의 Constant Pool(상수 풀) 테이블 내의 정보가 런타임 상수 풀에 로드되어 사용된다.
클래스 파일의 Constant Pool 테이블은 런타임 상수 풀을 구성하는 데 필요한 정보를 포함한다. 즉, 클래스 파일에 저장된 상수 풀 테이블의 데이터는 JVM이 클래스를 로드할 때 런타임 상수 풀로 변환되어 사용된다.
- 상수 풀(Constant Pool): Class File
- 상수 풀은 클래스 파일의 일부로, 컴파일된 자바 클래스 파일에 포함된다.
- 클래스 파일에는 클래스의 메타데이터뿐만 아니라, 클래스에 대한 리터럴 값, 메서드 및 필드에 대한 참조 등 모든 상수들이 상수 풀에 저장된다.
- 상수 풀은 클래스 파일의 구조 중 하나이며, 클래스 파일이 로드될 때 메모리에 적재된다.
- 클래스 파일이 로드될 때 상수 풀은 불변하며, 클래스의 생명주기 동안 변경되지 않는다.
- 런타임 상수 풀(Runtime Constant Pool): JVM
- 런타임 상수 풀은 JVM(Java Virtual Machine)이 실행되는 동안 메모리에 생성되는 공간이다.
- 클래스를 JVM이 로드할 때 클래스 파일의 상수 풀에서 필요한 정보들이 런타임 상수 풀로 복사된다.
- 런타임 상수 풀은 클래스 파일의 상수들을 저장하고, 클래스의 인스턴스화 및 메서드 호출과 같은 런타임 동작에 사용된다.
- JVM이 실행되는 동안 런타임 상수 풀은 계속 유지되며, JVM이 종료되면 메모리에서 해제된다.
즉, 상수 풀은 클래스 파일에 저장되어 클래스의 구조를 정의하고, 런타임 상수 풀은 실행 중에 클래스의 상수들을 저장하고 사용한다. 상수 풀은 클래스 파일의 구성 요소이며, 런타임 상수 풀은 JVM이 실행되는 동안 클래스의 상수들을 관리하는 데 사용된다.
🤔 그럼 Runtime Constant Pool 과 String Constant Pool 은 다른 건가?
이것도 서로 관련은 있지만 다른 개념이다. 자바에서는 문자열 리터럴을 저장하는 독립된 영역을 String Constant Pool(또는 String Pool)이라고 부른다. String은 불변객체이기 때문에 문자열 생성 시 이 String Constant Pool에 저장된 리터럴을 재사용해 메모리를 절약할 수 있다.
String str1 = "madplay"; // String Constant Pool 이라는 영역에 저장해 스트링 상수값으로 관리
String str2 = "madplay"; // 기존 "madplay" 상수 재활용. str 1이랑 같은 메모리 참조
String str3 = new String("madplay"); // 다른 객체와 마찬가지로 Heap 영역에 할당
String str4 = new String("madplay"); // 새로운 객체를 생성. str3이랑 다른 메모리 참조
System.out.println(str == str2); // true (같은 객체를 재사용하기 때문에)
System.out.println(str == str3); // false
System.out.println(str.equals(str3)); // true
그럼 Constant Pool에 저장되는 상수와 String Constant Pool에 저장되는 리터럴은 어떤 차이점이 있을까?
상수는 '초기화 이후 값이 변하지 않는 수'를 의미하고, 리터럴은 상수의 일종이지만, 선언 없이 바로 사용할 수 있는 문자 그대로의(=리터럴 한) 상수를 의미한다. 이렇게만 설명하면 아직 헷갈리니 예시를 들어 둘을 구분하자면
var a = 10; // a는 변수, 10은 리터럴이다.
var name = "Simba"; // name은 변수, "Simba"는 리터럴이다.
const pi = 3.14; // pi는 상수, 3.14는 리터럴이다.
const DRINKING_AGE = 21;
const VOTING_AGE = 18;
// 18과 21은 리터럴이다.
// 리터럴은 if(age > 18) 또는 if(age < 21)과 같이 프로그램의 모든 영역에서 사용될 수 있다.
// 하지만 상수를 이용하면 if(age > VOTING_AGE)와 같이 코드를 더 이해하기 쉽게 만들 수 있다.
이렇게 구분할 수 있다. Java는 기본 자료형을 제외한 객체를 만들 때, new 키워드를 이용해 참조형으로 만든다. 하지만, String은 예외적으로 new 키워드 없이 객체를 만들 수 있다. 이를 문자열 리터럴 생성 방식이라고 한다.
📚 참고
JEP 122: Remove the Permanent Generation
What is the difference between PermGen and Metaspace?
[JAVA] Java8부터는 static이 heap영역에 저장된다?
[Java] 많이 헷갈려하는 String constant pool과 Runtime Constant pool, Class file constant pool
Confusion between constants and literals?
Why concatenation of String object and string literal is created in heap? [duplicate]
Where does Java's String constant pool live, the heap or the stack?
'Study' 카테고리의 다른 글
[오픈소스] Spring Boot 프로젝트 컨트리뷰터 되기 (0) | 2024.04.29 |
---|---|
[Java] Method Area는 Heap 영역이 아니다! (1) | 2024.02.19 |
[Java] 변수와 객체 데이터 저장 (0) | 2024.02.17 |
[DB] 트랜잭션의 격리 수준(Isolation Level)이란? (0) | 2024.02.16 |
[Java] 객체비교 시 equals()와 hashcode() 둘 다 재정의해야 하는 이유 (0) | 2024.02.13 |