Java - String 깊게 이해해보기

 

Java String에 대해 어느 정도 알고 있다면 3번부터 읽는 걸 추천드립니다.

 

 

1. String의 불변성

 

JAVA의 String을 배우면 보통 String은 불변(Immutable, unchanged)하다는 것을 배우게 될 것이다.

 

이 불변이라는 말을 처음 들으면 오해하기 쉽다. (call by value, call by reference가 그렇듯이)

 

예를 들어

 

String test = "hello";
test = "changed?";

와 같은 코드는 당연히 작동한다.

 

그렇다면 string이 변한 것 아닌가..? 라고 생각할 수 있다.

그건 test 변수에 대해 오해하고 있기 때문이다.

여기서 test 변수는 object가 아니라 object의 reference이다.  그리고 우리가 변했다고 생각한 "hello"와 "changed"는 변하지 않은 채 각각의 String object로 존재하고 있다. (물론 기존 hello는 gc의 대상이 될 수 있을 것이다.)

그럼 그 object는 어디에 있을까?

 

2. JAVA는 String을 메모리 상에서 어떻게 관리할까?

 

아래 그림이 JAVA가 String을 어떻게 관리하는지 잘 보여준다고 생각한다. 

출처 : http://www.journaldev.com/797/what-is-java-string-pool

 

Java의 string은 참조 자료형이고 같은 문자열을 참조할 경우 String pool에서 동일한 메모리 값을 가리킨다. 예를 들어

String a = "hello";
String b = "hello";
System.out.println(a == b ? 1 : 0);

는 1을 출력한다. 

(String 값이 같으니 당연한 거 아니야?라고 물을 수 있지만 저 a == b 비교는 그 둘의 String 값을 비교하는 것이 아닌 a와 b의 주소값을 비교하는 것이다.)

 

 

그렇다면 아래 코드는 어떨까?

String a = "hello";
String b = new String("hello");
System.out.println(a == b ? 1 : 0);

이 경우 0을 출력한다. 

new로 heap에 메모리 공간을 따로 할당하였다고 생각하면 된다. 그렇기 때문에 기존의 String constant pool이 아닌 heap의 임의의 공간에 따로 저장된다. 따라서 주소값이 다르다.

 

다만 위 코드에서 2번째 줄에 intern 메소드를 삽입하면

String a = "hello";
String b = new String("hello").intern();
System.out.println(a == b ? 1 : 0);

기존대로 String constant pool에 저장되어 1이 출력된다.

 

사실 리터럴 형식 ("")으로 호출하는 방식 자체가 String에서 내부적으로 intern() 메소드를 호출시키는 방식으로 동작한다. 즉 같은 일을 하고 있다. (String constant pool에 존재하는지 검색 후 있다면 그 주소값을 반환, 없다면 String constant pool에 넣고 새로운 주소값 반환)

-------------------------------------------

+a

아래 내용은 참고로 읽어주세요.

 

참고로 JAVA 7까지는 String contant pool은 PermGen의 영역이었다. PermGen은 그 사이즈가 고정적이다. 하지만  PermGen의 경우 클래스 메타 데이터까지 담당하고 있기 때문에 공간 크기 예측이 정말 쉽지 않다. (이전 글 참조) 따라서 사용하기에 따라 OutOfMemoryException을 쉽게 발생시킬 수 있다.

그래서 String constant pool의 경우 heap 영역으로 변경되었다.

참고로 JAVA 8부터 PermGen는 사라지게 된다. 대신 metaspace라는 가변적인 메모리 공간으로 대체되었다. metaSpace의 경우 클래스 메타 데이터를 native 메모리에 저장하고 부족할 경우 자동으로 늘려주게 된다.

유념할 것은 String constant pool은 metaspace가 아닌 heap으로 이동하였다는 점이다.

 

출처 : https://stackoverflow.com/questions/42247199/string-pool-in-java-8

https://blog.voidmainvoid.net/315

---------------------------------------------

 

3. 예상되는 문제점 

 

String의 객체 다루는 방식을 보면서 다음과 같은 의문이 들었다.

메모리 효율을 위해 위와 같은 방식을 사용한다고 했지만 String을 이어붙일 때 메모리 낭비가 너무 심하지 않을까?

예를 들어 다음 코드가 있다.

String test = "";
for(int i=0; i<=10000; i++){
	test += "hi";
}

위의 코드는 그렇다면 10000번의 연산을 하는 동안 매번 새로운 객체를 만들기 때문에 기존의 객체는 계속 버려지고 GC의 대상이 계속 생겨나게 된다. GC가 잦아질수록 성능에 영향을 끼치기 때문에 이는 적절하다고 할 수 없다.

 

알아본 결과 JDK 5.0 이상에서는 위와 같은 연산을 자동으로 Stringbuilder 객체의 append 메소드를 사용하도록 반환한다고 한다. 

그 과정을 통해 객체를 한 번만 생성하여 버려지는 객체가 없도록 한다.

 

출처 : https://medium.com/@joongwon/string-%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EC%B0%B0-57af94cbb6bc

 

4. String은 컴파일 타임? 런타임? 어느 타이밍에서 처리되나요?

 

마지막으로 아래 코드는 어떻게 출력될까?

String tmp = "A";
String a = "AB";
String b = tmp + "B";
String c = "A" + "B";
String[] arr = {a, b, c};

// 출력 부분
for(int i=0 ; i< 3; i++){
   for(int j=0; j<3 ;j++){
		System.out.print(arr[i] == arr[j] ? 1:0);
        }
        System.out.println();
}

정답은

101

010

101 

이다.

 

즉 a와 c가 주소값이 같고 b는 다르다.

이는 리터럴 만을 +로 이은 변수 'c'와 변수와 리터럴을 이은 변수 'b'는 처리되는 방식이 다르기 때문이다.

c의 경우 컴파일 타임에서 먼저 리터럴을 합쳐 하나의 String을 만드는 반면

b의 경우 변수가 들어가기 때문에 컴파일 타임에 미리 합칠 수 없다. 변수는 바뀔 수 있기 때문이다. 따라서 런타임에

처리가 되며, 런타임에 만들어진 것은 String constant pool에 들어가지 않는다. 만약 런타임 동안 pool에 넣고 싶다면

.intern()으로 강제로 넣거나 혹은 위의 tmp 변수를 final로 선언하여 변경되지 않을 것을 보장해야 한다.

 

출처 : https://stackoverflow.com/questions/11989261/does-concatenating-strings-in-java-always-lead-to-new-strings-being-created-in-m