Java – Constant pool과 String pool

> String 리터럴에 대한 의문

  아래와 같은 코드를 보자.

public class StringPoolTest {

	public static void main(String[] args) {

		StringPoolTest spt = new StringPoolTest();
		spt.stringTest("HelloWorld");

	}

	public void stringTest(String helloWorld){
		StringTestArea sta = new StringTestArea();

		System.out.println( sta.TestString1().equals(helloWorld) );

	}
}


public class StringTestArea {

	public String TestString1(){
		String str = "HelloWorld";
		return str;
	}

}

코드 안에 계속해서 등장하는 String 리터럴은 (“HelloWorld”) 어떻게 처리가 될까? String type의 변수에 값을 할당하거나, 혹은 할당하지 않더라도 6번 Line과 같이 코드에 등장하는 String 리터럴은 계속해서 Runtime 시점에 메모리 공간을 할당 받아 점유할까?  stringTest() 메서드를 여러번 호출한다면 어떻게 될까? 호출할때마다 리터럴 “HelloWorld”은 어딘가 메모리에 로드되고 또 GC 대상이 될까?

test1 변수에 할당된 리터럴 “HelloWorld” 와 아래의 변수에 할당되지는 않았지만, System.out.println() 메서드의 파라메터가 되는 “HelloWorld”는 완전하게 동일한데 중복된 값이 메모리의 여기 저기에 로드 되고 있는 걸까?


> Constant pool ? 

  Java Class File의 구성 항목 중 하나인 Constant pool은 리터럴 상수 값을 저장하는 곳이다.  여기에는 String 뿐 아니라, 모든 종류의 숫자, 문자열, 식별자 이름, Class 및 Method 에 대한 참조와 같은 값이 포함된다.  Constant pool은 특정 상수에 대한 모든 인덱스 또는 참조를 16비트(type u2) 번호로 제공되며, 여기서 인덱스 값 1은 표의 첫 번째 상수를 나타낸다.

그럼 위의 소스에 대해 javap를 이용해서 Class의 구조와 Byte code를 확인해보자.  ( ※ javap – The Java Class File Disassembler)

C:\TestBoard>javap -verbose -private StringPoolTest
Classfile /C:/Users/USER/IdeaProjects/TestBoard/out/production/TestBoard/StringPoolTest.class
  Last modified 2018. 12. 20; size 722 bytes
  MD5 checksum 8c7857ce091e30c9c6e124e1d89e5d60
  Compiled from "StringPoolTest.java"
public class StringPoolTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#26         // java/lang/Object."<init>":()V
   #2 = Class              #27            // StringPoolTest
   #3 = Methodref          #2.#26         // StringPoolTest."<init>":()V
   #4 = Methodref          #2.#28         // StringPoolTest.stringTest:()V
   #5 = String             #29            // HelloWorld
   #6 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Methodref          #32.#33        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #34            // java/lang/Object
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               LStringPoolTest;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               spt
  #21 = Utf8               stringTest
  #22 = Utf8               test1
  #23 = Utf8               Ljava/lang/String;
  #24 = Utf8               SourceFile
  #25 = Utf8               StringPoolTest.java
  #26 = NameAndType        #9:#10         // "<init>":()V
  #27 = Utf8               StringPoolTest
  #28 = NameAndType        #21:#10        // stringTest:()V
  #29 = Utf8               HelloWorld
  #30 = Class              #35            // java/lang/System
  #31 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #32 = Class              #38            // java/io/PrintStream
  #33 = NameAndType        #39:#40        // println:(Ljava/lang/String;)V
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Utf8               (Ljava/lang/String;)V
{
  public StringPoolTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   LStringPoolTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #2                  // class StringPoolTest
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #4                  // Method stringTest:()V
        12: return
      LineNumberTable:
        line 5: 0
        line 6: 8
        line 8: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            8       5     1   spt   LStringPoolTest;

  public void stringTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #5                  // String HelloWorld
         2: astore_1
         3: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: aload_1
         7: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: ldc           #5                  // String HelloWorld
        15: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        18: return
      LineNumberTable:
        line 11: 0
        line 13: 3
        line 15: 10
        line 17: 18
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  this   LStringPoolTest;
            3      16     1 test1   Ljava/lang/String;
}
SourceFile: "StringPoolTest.java"

39번 Line   [ #29 = Utf8  HelloWorld ] 를 보면 Constant pool에 해당 “HelloWorld” 리터럴이 한번만 들어간걸 확인할 수 있다.  아마도 Compiler Optimization 과정 중 이루어진 일로 추정된다.  그리고 해당 리터럴을 사용하는 메서드에서는 Constant pool의 인덱스를 참조한다.   

  결국 코드에 등장하는 리터럴 들은 Class 단위로 Constant Pool에서 관리가 이루어지고, 한 Class에 등장하는 동일한 리터럴 값은 여러번 메모리에 Load되지 않음을 알 수 있다.   Runtime 시점에 해당 리터럴에 대한 참조는 Class가 Load되어 있는 JVM 메모리 영역 (Java 1.7 이전은 Permanent 영역, Java 8 이후는 Metaspace 영역) 에서 이루어진다고 볼 수 있다.

 

> String Pool ?

  그런데 아래의 코드를 한번 보자.

public class StringPoolTest {

	public static void main(String[] args) {

		StringPoolTest spt = new StringPoolTest();
		spt.stringTest("HelloWorld");

	}

	public void stringTest(String helloWorld){
		StringTestArea sta = new StringTestArea();

		System.out.println( sta.TestString1() == helloWorld );

	}
}


public class StringTestArea {

	public String TestString1(){
		String str = "HelloWorld";
		return str;
	}

}


결과  (StringPoolTest 의 “HelloWorld”와 StringTestArea의 “HelloWorld”가 동일한 것으로 확인됨 )

true

Process finished with exit code 0


“==” 비교를 이용했음에도 불구하고 각기 다른 Class의 Constant String 리터럴이 완전하게 동일한 것으로 비교된다.  

이는 Java String 처리의 특징 중에 하나인 String Pool을 이용하기 때문이다.

String은 다른 int, boolean, long과 같이 기본 자료형 (primitive) 이 아니다.  그러나 기본 자료형과 같이 리터럴을 할당할 수 있다.  그리고 JVM은 이렇게 리터럴이 할당된 String 객체는 String Pool이라는 테이블에 저장하고 만약 이미 String Pool에 해당 값이 존재할 경우 해당 인덱스만 참조하도록 한다.  (== String.intern() 메서드의 효과와 동일하다.)

그런데, 만약 String 변수에 리터럴을 할당하지 않고 new String() 을 이용해 스트링 객체를 생성하거나, String world = “World”; “Hello” + world; 와 같이 문자열을 조립하는 경우 다른 결과를 보인다.

아래의 코드 결과를 확인해보자.  결과가 true인 경우를 살펴보면 어떤 경우에 String pool 테이블이 이용됐는지 알 수 있다.

public class StringPoolTest {

    public static void main(String[] args) {
        String helloWorld = "HelloWorld";
        String world = "World";
        System.out.print((helloWorld == "HelloWorld") + " ");
        System.out.print((Other.helloWorld == helloWorld) + " ");
        System.out.print((helloWorld == ("Hello"+"World")) + " ");
        System.out.print((helloWorld == ("Hello"+world)) + " ");
        System.out.print((helloWorld == ("Hello"+world).intern()) + " ");
        System.out.println(helloWorld == new String("HelloWorld"));
    }

}

class Other { public static String helloWorld = "HelloWorld"; }


결과

true true true false true false

Process finished with exit code 0