본문 바로가기
Java/기초

[Java] 많이 헷갈려하는 String constant pool과 Runtime Constant pool, Class file constant pool

by EricJeong 2023. 5. 19.

String Constant Pool과 Constant Pool

이 두 가지는 완전히 다른 개념입니다. 용어가 비슷한 형태이기 때문에 이 두 가지를 혼용하여 헷갈리는 경우가 많습니다만, 저장되는 위치부터 저장하는 데이터의 종류, 관리 주체까지 모든 것이 다른 저장공간입니다.

 

심지어 Constant pool은 JVM - Metaspace영역의 Constant pool과 컴파일된 class파일의 Constant pool이 다릅니다. 헷갈리지 않도록 살펴봅시다.

 

 

String Constant Pool

Java에서 문자열 리터럴을 저장하는 독립된 영역을 `String Constant Pool` 또는 `String Pool`이라고 부릅니다.

JVM - Perm/Metaspace 영역에 존재하고 일반적으로 GC 대상이 되지는 않습니다. String은 불변객체이기 때문에 문자열의 생성 시 이 String Constant Pool에 저장된 리터럴을 재사용할 수 있습니다. 

 

[참고] String constant pool은 일반적으로 GC가 되지 않지만, 문자열 참조 대상이 없는 경우 선택적으로 GC 대상이 되기도 합니다. 
또한, -XX:+UseStringDeduplication옵션 사용을 통해 중복 문자열에 대해 GC를 수행하도록 트리거를 줄 수 있습니다. 자세한 내용은 여기를 참조해주세요.

 

소스코드에서 리터럴("Hello")로 선언된 문자열이 이 저장공간에 저장되며 동적으로 생성된 문자열은 저장되지 않습니다. 그리고 한번 저장된 문자열은 소스코드에서 동일한 문자열에 대해 재사용하여 메모리를 절약하는 역할을 합니다.

 

String constant pool에 문자열이 저장되는 조건은 아래와 같습니다.

 

1. 소스코드에 문자열을 선언한 경우

String str = "Hello world!";  // String constant pool에 저장됨
String doNotUse = new String("Hello World!");  // heap 영역에 저장됨

소스코드에 리터럴 형태로 작성한 문자열은 컴파일 시 컴파일러에 의해 string constant pool 저장 대상으로 표시됩니다.

코드 클래스 파일의 상수 풀(아래에서 나올 constant pool과 다름)에 `CONSTANT_String`타입으로 저장되며 런타임시 스캔 대상이 되어 string constant pool로 저장하는 구조입니다.

이렇게 String constant pool에 저장된 문자열은 모두 같은 객체를 재사용하게 됩니다. 즉, 아래와 같은 결과가 나옵니다.

String str = "hello"; // String constant pool에 저장
String str2 = "hello"; // String constant pool에서 재사용
String str3 = new ("hello"); // 별도의 Heap 메모리에 저장

System.out.println(str == str2);	// true (같은 객체를 재사용하기 때문에)
System.out.println(str == str3);	// false
System.out.println(str.equals(str3));	// true

 

 

new 연산자를 사용하여 문자열 생성시 string constant pool을 사용하여 최적화가 가능할 수 있던 문자열을 heap영역으로 신규 저장하게 됩니다. 그러므로 성능 및 메모리 최적화를 위하여 문자열 선언 시 new 연산자 사용은 지양하는 것이 좋습니다.

 

2. String.intern() 메서드를 실행한 경우

new String("Hello World").intern(); // String pool에 강제로 넣음
new String("World").intern(); // String pool에 강제로 넣음
new String("Hello World").intern(); // 이미 String pool에 있기 때문에, 추가되지 않음

intern 메서드는 String constant pool에 해당 문자열이 있는지 검증하고 없으면 상수 풀에 저장한 후 레퍼런스를 전달하고, 있으면 기존 상수풀에 있는 레퍼런스 값을 넘겨줍니다. 특정 문자열을 강제로 string constant pool에 넣기 때문에 정해진 문자열이 아닌 동적인 문자열에 이 intern() 메서드를 사용하면 상수풀이 사용하는 메모리가 계속해서 커집니다. 또한, 이 메서드는 비싸고 느리기 때문에 사용이 권장되지는 않습니다.

 

그렇기 때문에 어떤 경우에도 절대 사용하지 않는 것이 좋습니다. intern 메서드를 사용하는 경우 String constant pool의 크기가 계속해서 커지며, 메모리가 부족해질 수 있습니다. 그리고 이 저장공간은 어지간해서는 GC 대상이 되지 않고 애플리케이션이 종료될 때까지 메모리에 남아있습니다. 즉, 메모리 릭의 발생 가능성을 높이는 메서드이므로 사용하지 않는 것이 좋습니다.

 

주의할 점

동적으로 생성되는 문자열의 경우에는 String constant pool에 저장되는 대상이 아닙니다. 즉, ORM, MyBatis등 DB에서 조회해 오는 String, File에서 읽어 들이는 String 등은 string constant pool에 저장되는 대상이 아닙니다. 그러므로 이런 동적인 문자열은 동일한 문자열이어도 동일연산(==)의 결과는 false이므로 문자열 비교는 동등연산(equals)을 통해 진행해야 합니다. 

 

Constant Pool (Class file)

컴파일시 클래스파일 내부에 존재하는 영역으로, 클래스로더에 의해 JVM에 로드될 때 메모리에 로드합니다. 주로 클래스의 구성요소(상수, 문자열, 클래스/인터페이스 참조) 데이터를 저장하고 있습니다. Oracle 블로그를 보면 다음과 같이 설명하고 있습니다.

 

다음 클래스파일의 경우 Class Constant Pool이 이렇게 표기됩니다.

class Hello {
    public static void main( String[] args ) {
        for( int i = 0; i < 10; i++ )
            System.out.println( "Hello from Hello.main!" );
  }
}

 

위 클래스를 컴파일한 후, 상수풀을 열어보면 이런 상수들로 저장되어 있습니다.

  #1 = Methodref          #6.#16         // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#18        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #19            // Hello from Hello.main!
   #4 = Methodref          #20.#21        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #22            // Hello
   #6 = Class              #23            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               StackMapTable
  #14 = Utf8               SourceFile
  #15 = Utf8               Hello.java
  #16 = NameAndType        #7:#8          // "<init>":()V
  #17 = Class              #24            // java/lang/System
  #18 = NameAndType        #25:#26        // out:Ljava/io/PrintStream;
  #19 = Utf8               Hello from Hello.main!
  #20 = Class              #27            // java/io/PrintStream
  #21 = NameAndType        #28:#29        // println:(Ljava/lang/String;)V
  #22 = Utf8               Hello
  #23 = Utf8               java/lang/Object
  #24 = Utf8               java/lang/System
  #25 = Utf8               out
  #26 = Utf8               Ljava/io/PrintStream;
  #27 = Utf8               java/io/PrintStream
  #28 = Utf8               println
  #29 = Utf8               (Ljava/lang/String;)V

 

 

Runtime Constant Pool (JVM)

상수 풀, Runtime Constant Pool이라고도 부르며 Java 8 이전에는 JVM - Perm영역에 저장되었고, Java8 출시 이후부터는 JVM - Metaspace 영역에 저장됩니다. 앞서 설명한 Class file constant pool이 런타임시 이 영역으로 저장됩니다.

 

주로 클래스와 관련된 메타데이터를 저장하고 클래스 구조, 필드, 메서드와 같은 데이터를 저장합니다. Runtime 로딩 과정은 이 문서를 참고하시면 됩니다. 클래스 상수 풀 또한 클래스로더에 의해 클래스를 로딩할 때 Runtime Constant Pool에 저장됩니다.

 

Metaspace 영역으로 이관하게 된 이유

Perm영역에 이 데이터가 저장되던 시점(JDK 7 이하)에는 Class, Metadata 로딩 과정에서 메모리 릭이 발생하였고, Perm 영역의 크기를 고정적으로 설정해야 했기 때문에 메모리 부족으로 OOM이 터지는 일이 있었습니다. 이 이슈를 개선하기 위해 Constant Pool을 Metaspace영역으로 이관하였고 OOM을 피할 수 있게 되었습니다. 조금 더 설명하자면 Metaspace영역은 JVM의 Native Memory를 사용하며 JVM이 관리합니다. Perm영역과의 결정적인 차이는 메모리가 동적으로 관리되며 필요할 경우 OS에게 요청하여 메모리를 추가 할당할 수 있습니다. 이를 통해 OOM을 개선할 수 있었습니다.

 

결론 정리

  String Constant Pool Constant Pool inside a Java class file Runtime Constant pool
역할 String 객체의 상수 값을 저장(캐싱) 클래스 파일의 상수 값을 저장 Class Constant pool에서 읽어온 상수 값, 클래스 메타데이터 저장
저장되는 값 String 상수 값
ex) "Hello", "World"
클래스 파일에 포함된 상수  값
ex) 123, 3.14, true
클래스 파일에 포함된 상수 값. Class Constant pool에 저장되어있던 값이 런타임시 이 영역으로 저장된다.

저장 위치 Java 7 이전 - Perm
Java 8 이상 - Metaspace
Class file Java 7 이전 - Perm
Java 8 이상 - Metaspace
저장 트리거 String.intern(), String을 리터럴로 생성 컴파일시 생성됨 클래스파일에 코드 레벨로 선언된 상수풀이 런타임시 로더의 판단에 의해 올라옴. 
불변 여부 불변 (Immutable) Class 파일 자체는 불변. Runtime시 동적 로드에 의해 변경될 수 있음 불변이 아님.클래스파일이 동적으로 로딩되고 초기화되기 때문에, 로드될 때 마다 변경됨

 

 

댓글