String Constant Pool에 대해 살펴보기 전에, 다음 하나만 참고하시라고 적어두고 시작하겠습니다.
문자열 리터럴을 통해 문자열을 생성하는 방식은 다음과 같습니다.
String str = "literal";//여기서 "literal"이 리터럴
이제 시작하겠습니다.
String Constant Pool
String은 Java에서 가장 많이 사용되는 클래스입니다.
지금부터 JVM에서 String을 저장하는 특수한 메모리 영역인 String Constant Pool에 대해서 살펴보겠습니다.
String 재사용(Interning)
Java에서 String은 불변(immutable) 객체입니다. 이 덕분에 JVM은 String Constant Pool에 각 문자열 리터럴의 복사본을 하나만 저장하여, 문자열에 할당되는 메모리 양을 최적화할 수 있습니다.
그리고 해당 과정을 재사용(Interning)이라고 합니다.
String 변수를 만들고 값을 할당할 때 JVM은 String Constant Pool에서 동일한 값의 String을 검색합니다.
만약 동일한 값을 가진 String이 발견된다면 자바 컴파일러는 추가 메모리를 할당하는 것이 아니라, 단순히 해당 String의 주소에 대한 참조를 반환합니다.
만약 존재하지 않는다면 해당 값은 String Constant Pool에 추가되고(인턴) 해당 값에 대한 참조가 반환됩니다.
생성자를 사용한 문자열
new 연산자를 통해 문자열을 생성한다면 Java 컴파일러는 다른 일반적인 객체들처럼 새 객체를 생성한 후 Heap 메모리 영역에 저장합니다.
new 연산자를 통해 생성된 모든 문자열은 고유한 주소를 가진 다른 메모리 영역을 가리키게 됩니다.
literal을 사용한 문자열 VS 생성자를 사용한 문자열
new 연산자를 사용하여 String 객체를 생성하면 항상 Heap 메모리 영역에 새 객체를 생성하여 저장합니다.
반면 String 리터럴을 사용하여 객체를 생성하는 경우, 만약 해당 문자열이 String Constant Pool에 이미 존재하는 문자열이라면 기존에 생성해 둔 객체를 반환할 수 있습니다. 만약 존재하지 않더라도, 새롭게 String 객체를 String Constant Pool에 저장함으로써 이후 똑같은 문자열 리터럴을 사용하였을 때 기존 값을 재사용 할 수 있습니다.
따라서 정말 특수한 경우가 아니고서야 String은 new 생성자 대신 문자열 리터럴을 사용해야 합니다.
가비지 컬렉터
Java 7 이전에는 JVM이 고정된 크기를 가진 PermGen이라는 공간에 String Constant Pool을 배치했습니다.
PermGen이라는 공간은 크기가 고정되어 있어서 런타임 시 확장이 불가능하며 garbage collection에 적합하지 않습니다.
PermGen에서 너무 많은 문자열을 재사용(interning)하는 경우 JVM에서 OutOfMemory 오류가 발생할 수 있습니다.
Java7 이후부터 String Constant Pool은 JVM에 의해 Garbage Colleted되는 Heap 메모리 영역에 배치되었습니다.
Heap 영역에 배치되는 것의 장점은, 참조되지 않은 String은 GC에 의해 String Constant Pool에서 제거되어 메모리가 해제되기 때문에 OutOfMemory 오류의 위험이 감소한다는 점입니다.
(참고로 PermGen은 Java8부터 Metaspace로 대체되었습니다.)
참고 - Compact Strings
Java 8버전까지는 String은 내부적으로 UTF-16으로 인코딩된 문자 배열인 char[]로 표현되어 대부분의 언어가 1바이트로 표현이 가능했음에도 불구하고 2바이트의 메모리를 사용했습니다.
그러나 Java 9에서는 Compact Strings를 제공함으로써, 저장된 문자열의 내용에 따라 char[]과 byte[] 사이에서 적절한 인코딩을 선택하여 사용합니다.
따라서 Heap 메모리 사용양이 상당히 줄어들었고, JVM에서 GC 오버헤드가 줄어들었습니다.
왜 String Constant Pool은 Heap 영역에 존재할까?
자바에서는 객체를 생성하거나 변수를 선언할 때마다 메모리에 저장됩니다.
자바에서 메모리는 Stack과 Heap영역, 이렇게 두 가지 영역으로 나눕니다.
Stack 영역과 Heap 영역은 서로 저장하는 데이터가 다르며, 데이터를 저장하는 방법과 데이터에 접근하는 방식이 다릅니다.
위에서도 배웠지만 String 리터럴(literal)을 선언할 때, JVM은 해당 String 객체를 이곳(String Constant Pool)에 저장하고, Stack 메모리 영역에서 이를 참조합니다.
(추가로 지금부터 설명하는 String 생성은 특별한 언급이 없는 이상 모두 리터럴로 생성되는 String입니다.)
각각의 String 객체를 메모리에 생성하기 전에, JVM은 메모리의 오버헤드를 줄이기 위해 몇가지 과정을 수행합니다.
String constant pool은 HashMap으로 구현되어 있습니다. Hashmap 각각의 bucket(저장공간)은 같은 해시코드를 가진 String list를 저장합니다. (옛날 버전의 자바에서는 String constant pool의 저장공간은 고정된 크기였으며, 종종 "객체 Heap을 저장하기 위한 충반한 공간이 없습니다"라는 오류를 발생시켰습니다.)
시스템이 클래스를 로드할 때(클래스 로더에 의해 로드될 때) 모든 클래스의 문자열 리터럴은 application-level pool(String constant pool)로 이동하는데, 이는 서로 다른 클래스의 String 리터럴들은 동일한 객체여야 하기 때문입니다.
그리고 현재 상황(String 리터럴들이 String constant pool로 이동한 상황)에서, pool속에 들어있는 데이터들은 어떠한 종속성 없이도 각각의 Class들에서 사용이 가능해야 합니다.
일반적으로 Stack 메모리 영역에는 수명이 짧은 데이터들이 저장됩니다. 지역변수, (Heap에 저장된 실제 객체의 주소값을 참조하는)참조변수, 그리고 실행중인 메소드들이 Stack 메모리 영역에 저장되는 데이터들입니다.
Heap 메모리 영역은 동적인 메모리 할당을 허용하며 런타임 시 Java의 객체 및 JRE 클래스를 저장합니다.
(참고 - JER이란? )
Heap 메모리 영역은 어디에서나 접근할 수 있으며, 어플리케이션이 실행되는 동안의 모든 쓰레드에서 데이터를 저장하는 것이 가능합니다. (반면 Stack은 각각의 Thread별로 할당되며, 다른 Thread의 Stack 메모리 영역에는 접근할 수 없고, 오로지 자신의 Stack 영역에만 접근할 수 있습니다)
Stack 메모리 영역은 데이터를 인접한 메모리 블록(contiguous memory blokcs)에 저장하고 랜덤한 접근을 허용합니다.
만약 String constant pool이 Stack 메모리 영역에 존재한다면, 클래스에서 임의의 String을 필요로 할 때 Stack 자료구조의 LIFO(후입선출) 특성 때문에 이용할 수 없을 가능성이 있습니다.
이와는 대조적으로 Heap은 메모리를 동적으로 할당하고 어떤 방식으로든 데이터에 접근할 수 있도록 해줍니다.
다양한 타입의 변수로 구성된 코드 조각(snippet)이 있다고 가정해 보도록 하겠습니다. (아래 사진입니다.)
Stack 메모리 영역에서는 int 리터럴의 값(num = 50)과 String 및 Demo Object의 참조값을 저장합니다.
변수들은 Stack 메모리 영역에 생성되었다가 Thread의 실행이 완료되는 즉시 할당이 해제되는 반면
Heap 메모리 영역의 자원들은 GC(가비지 컬렉터)에 의해 회수됩니다.
또한 GC는 String constant pool에서 참조되지 않는 자원들도 회수합니다.
String constant pool의 기본 크기는 플랫폼마다 다를 수는 있지만, Stack의 크기보다는 훨씬 큽니다. JDK 7버전 이전에는 String constant pool은 permgen 공간의 일부였으며 그 이후부터는 메인 Heap 메모리 영역의 일부입니다.
📔 Reference
https://www.baeldung.com/java-string-constant-pool-heap-stack
https://www.baeldung.com/java-string-pool
'☕️ Java > 기본' 카테고리의 다른 글
[Java] 리플렉션을 활용한 백준 자동 README 생성기 (0) | 2022.02.07 |
---|---|
[Java] Method Signature와 Method Type (0) | 2022.02.06 |
[Java] String = "" 과 new String("")의 차이 (0) | 2022.01.28 |
[Java] final 키워드 (상수와 리터럴) (0) | 2022.01.28 |
[Java] static 파헤치기 (feat. Hiding (static Method의 Overriding)) (0) | 2022.01.24 |