glenn-syj / more-effective-java

이펙티브 자바를 읽으며 자바를 더 효율적으로 공부합니다
4 stars 5 forks source link

[MEJ-004] String Pool에 대한 보충 설명 #91

Closed glenn-syj closed 7 months ago

glenn-syj commented 7 months ago

based on: #81 by @undeadtimo

들어가며

String 자료구조에서 드러나는 불변성을 잘 지적해주셨는데요. JVM과 관련된 부분이기도 해, 이번 글에서는 String Pool에 대해 부가적인 설명을 하려고 합니다. 물론 String Pool의 이용이 불변성에서 비롯되는 것이기도 하고요.

String Pool이란

스트링 풀은 JVM 힙 메모리에 저장되는 String의 묶음이라고 볼 수 있습니다. String 객체를 생성하고 값을 할당하면, JVM이 그와 같은 값에 대한 String을 찾는데요. 만약 String Pool 내에서 해당 값이 발견된다면, 자바 컴파일러는 메모리에 대한 참조값만 반환합니다.

자바 7 이전에는 JVM에서 고정 크기를 가진 PermGen 공간에 String Pool을 두었습니다. 그래서 런타임 중에 메모리 공간이 확장될 수 없어, 에러가 나기도 했다고 합니다. 물론 자바 7 이후에는 힙 공간에 두었을 뿐만 아니라, 참조되지 않은 String을 가비지 콜렉팅할 수도 있게 되었습니다.

String Pool 심화

그러나 String Pool은 단순히 동등한(#90) 객체를 생성할 때마다 이용되는 것은 아닙니다. 아래 명시한 참고 자료에서 이해에 도움이 되는 코드가 있어 가져왔습니다.

// 1번 코드
String first = "Baeldung"; 
String second = "Baeldung"; 
System.out.println(first == second); // True
// 2번 코드
String third = new String("Baeldung");
String fourth = new String("Baeldung"); 
System.out.println(third == fourth); // False
// 3번 코드
String fifth = "Baeldung";
String sixth = new String("Baeldung");
System.out.println(fifth == sixth); // False

1번 코드는 두 String 객체 모두 리터럴을 이용해서 생성되었습니다. 2번 코드에서는 new 키워드와 함께 생성자가 이용되었구요. 3번 코드에서는 각기 따로 이용되었습니다. 위 1번~3번 코드에서 주석으로 처리된 결과에서 String 객체 생성 시에 String Pool이 이용되는 지 여부를 잘 드러냅니다.

즉, 리터럴을 이용한 생성에서는 String Pool에 있는 값을 재사용하는 반면, new 키워드와 생성자를 이용할 때에는 힙 메모리에 새로운 객체가 생성됩니다. 따라서 상황이 허락하는 한에서는 리터럴을 이용한 생성이 더욱 효율적이라는 결론으로도 이어집니다.

String Pool 조작

intern() 메소드를 이용하면 String Pool을 수동적으로 조작할 수 있습니다. 아래는 자바8 공식문서에서의 설명입니다.

public String intern() Returns a canonical representation for the string object. A pool of strings, initially empty, is maintained privately by the class String.

When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned. Otherwise, this String object is added to the pool and a reference to this String object is returned.

It follows that for any two strings s and t, s.intern() == t.intern() is true if and only if s.equals(t) is true.

All literal strings and string-valued constant expressions are interned. String literals are defined in section 3.10.5 of the The Java™ Language Specification.

Returns: a string that has the same contents as this string, but is guaranteed to be from a pool of unique strings.

즉, intern() 메소드는 String Pool에 해당 문자열이 (1) 등록된 경우와 (2) 등록되지 않은 경우에 다르게 동작합니다. (1) 등록된 경우에서는 intern() 메소드가 풀 내에 있는 String을 반환합니다. (2) 등록되지 않은 경우에 대해서는 String Pool에 등록한 뒤, 참조값을 반환합니다. 당연하게도, 이 과정에서는 동등성이 이용됩니다.

또한, 위 문서에 모든 리터럴을 이용한 String이 위 메소드와 같은 방식으로 동작한다고 명시 되어 있다는 점도 흥미롭습니다.

References

https://docs.oracle.com/javase/8/docs/api/java/lang/String.html https://www.baeldung.com/string/intern https://www.baeldung.com/java-string-pool

undeadtimo commented 7 months ago

Glenn-syj님의 String pool에 대한 상세한 보충 설명 감사드립니다.

막연하게, String pool 에 등록되어있는 값을 다른 String 에서 갖게 될 때, 두 String은 같은 참조값을 갖게 된다고만 인식하였는데, 리터럴과 생성자로 String 객체를 다루었을 때의 차이점을 세 가지 예제 코드로 설명해주셔서 원리를 제대로 이해할 수 있게 되었습니다.

생성자를 통해 String 객체를 생성할 경우, 기존의 String pool 내부에 같은 문자열에 대한 공간과 데이터가 존재한다고 하더라도, 새로운 메모리 공간이 힙에 할당되어 새로운 참조값을 가지게 되는 것을 알게 되었습니다.

86 에서 yngbao97님께서 올려주신 예제 코드와 함께 생각해보니 이해를 하는데 더욱 도움이 되었습니다.

String b = "헬로";
System.out.println(b.hashCode());       // 1744880

b = "안녕";
System.out.println(b.hashCode());       // 1611021

b += "하세요";
System.out.println(b.hashCode());       // 803356551

여기서 b += "하세요"는 비록 리터럴끼리의 연산이지만, 기존의 String pool에 "안녕하세요" 가 존재하지 않기 때문에, String pool 내부에 새로운 공간을 할당하여 "안녕하세요" 가 저장되기 때문에 다른 참조값을 나타내게 되는 것입니다.

이러한 String 클래스의 동작원리를 가능케하는 intern() 메서드를 추가로 알게되어, String과 String pool에 대한 내부적인 작업이 이루어질 때, 그 과정을 머릿속에 그릴 수 있게 되었습니다.