String 클래스는 불변 객체로 String 클래스의 문자열을 저장하는 char[]
을 보면 final
로 선언되어 있고, 해당 배열을 재할당하는 코드는 존재하지 않는다.
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. **/
private final char value[];
...
}
즉, 한 번 할당한 문자열을 중간에 변경할 수는 없기 때문에 우리는 매번 더하기 연산을 통하여 문자를 이어 붙여 매번 수정할 때마다 새로운 객체를 생성하여 재할당을 시킨다.😥
- 반복적으로 문자열을 이어 붙이다 보면 Heap 영역에서 참조를 잃은 문자열 객체가 계속해서 쌓이게 된다.
...
물론, 나중에 GC에 의해 수거가 되지만 메모리 관리 측면에서 이러한 코드는 결코 좋은 코드라고 할 수 없다. 또한 계속해서 객체를 생성하므로 연산 속도 측면에서도 성능이 떨어질 수 밖에 없다.
그렇다면 String Pool은?
= 연산자
를 통해 값을 String에 대입하면 Heap 영역 내에 있는 String Pool이라는 공간에 문자열이 저장된다.
new 연산자
를 통해 String을 만들면 String Pool이 아닌 일반 Heap 영역 어딘가에 저장된다.
둘다 Heap 영역에 저장되는 것은 동일한데, String Pool에 값이 저장되면 어떠한 이점이 있는 것일까?🤔
전자의 방식을 String literal
이라고 하는데, String literal로 생성한 객체는 String Pool의 '메모리 주소'를 가리키게 된다.
반면 후자는 일반적인 new 연산자를 통해 객체를 생성하는 방식이므로 String Pool의 해당 값이 있더라도 Heap 영역 내 별도의 메모리를 할당하여 주소를 가리키게 된다.
그럼에도 불구하고 문자열의 변화가 상당히 많아서 String 객체를 많이 생성하면 성능 이슈가 발생한다.
그러므로... String 객체를 하나로 두고, 내부 상태를 변경하는 가변 객체로 만드는 것이 좋은데, Java에서는 StringBuilder와 StringBuffer를 지원한다.
char[]
배열의 사이즈를 조절하여 문자열을 이어 붙이기 때문에 새로운 객체를 최소로 할당할 수 있다.StringBuilder s = new StringBuilder("hello");
System.out.println(s.hashCode()); // 859417998
s.append("world");
System.out.println(s.hashCode()); // 859417998
대부분의 메소드에 synchronized
가 적용되어 일반적으로 멀티 스레드 환경에서 스레드 안전하게 동작한다.
-> 동기화를 지원하는 StringBuilder
라고 생각하면 된다.
String Builder는 동기화를 지원하지 않는다.
그렇기에 멀티쓰레드에서는 적합하지 않으나, 단일 쓰레드에서는 String Buffer보다 훨씬 우수한 성능을 가진다.