Java - String

  • String 은 Java에서 아주 중요한 클래스이다.

1. String Basic

String str1 = "Hi";
String str2 = new String("Hi");
  • 이 두 변수는 오버라이드 된 equals.. 즉 동등 형식으로 비교해야지만, true가 나오며 동일(==) 형식으로 비교하면 false가 나온다.


String str1 = "Hi";
String str2 = "Hi";
  • 당연히 String으로 만든 객체니까 equals는 true가 나오고, == 는 false가 나올거라고 생각한 사람들이 많을 것이다.
  • 그러나 우리의 예상을 비웃듯 위 코드를 동일(==) 로 비교해도 true가 나온다.
  • 어떻게 된 일일까?



2. String Contant Pool 과 Heap 메모리

  • 일단 위 내용을 설명하기 위해서는 메모리에 대한 이야기를 할 필요가 있다.
  • 우리 JVM 안에는 여러 메모리 공간이 나뉘어져있는데, 그 중 객체의 값을 저장하는 Heap 메모리라는 것이 있고, 그 안에는 String 리터럴로 생성한 값을 저장하는 String Contant Pool 이라는 저장소가 있다.
    • 원래는 Heap 메모리 바깥에 자리했지만, JDK 7부터 Heap 메모리 안으로 들어오게 되었다. 그 덕에 Contant Pool 안의 String 객체들도 GC에 의해 할당 해제가 되면서 메모리 관리가 가능해졌다.
        String str1 = "Hi"; // 리터럴 o
        String str2 = new String("Hi"); // 리터럴 x
      
    • 이렇게 알고 있으면 된다.
    • 위에 처럼 생성된 값은 String Contant Pool에 저장이되며, 아래처럼 생성된 값은 Heap에 자리를 잡는다. 이 때 String Contant Pool은 이름 그대로 상수풀인데, 같은 값을 가리킬 경우 그 객체의 주소값이 모두 동일하다는 성질을 가지고 있다.
        String a = "Hi";
        String b = "Hi";
        String c = "Hi";
      
    • 즉 우리가 이렇게 작성하면 a b c 모두 같은 주소를 가리킨다는 의미이다.
    • 반대로 new String으로 저렇게 했다면, 각각 다른 주소를 가지고 있게 된다.
    • 덕분에 리터럴로 생성하게 되면, 메모리 비용을 아낄 수 있고, 좋은 점이 많다. 그래서 가급적 String을 선언할 때는 저렇게 리터럴 방식으로 선언을 해주는 것이 좋다.
    • 참고로 저 String Contant Pool은 자기를 참조하고 있는 주소가 모두 없어져야지만 GC의 대상이 된다. 즉, 위 코드의 a b c가 모두 다른 값이 되어야지만 GC의 대상이 되어 메모리에서 사라지는 것이다.



3. String에 +가 가능한데, 왜 불변 객체인가?

  • 우리는 String이 불변 객체라고 배웠다. 그런데 이상한 점이 왜 +로 합칠 수도 있고, 다시 재할당도 할 수 있는걸까?
      String a = "Java";
      a += " King";
    
  • 이런 코드가 있다고 가정하자. 이러면, a는 Java에서 Java King이 되었으니 수정된 거라고 생각할 수 있다.
  • 그러나 정확히는 a가 내부에서 Java King으로 다시 선언된 것이다.
  • 즉, 기존에 String Contant Pool의 “Java” 라는 것을 참조하던 주소가 새롭게 “Java King” 이라는 걸 참조하는 주소로 new String이 된 것이다.
  • 그 후 이제 “Java”는 참조하는 다른 주소가 없다면, 자연스럽게 GC의 대상이 된다.
  • 추가로, String은 불변 객체이므로, Thread-Safe 하다고 본다.



4. 메모리를 아끼는 방법

  • 위 내용을 좀 더 정확히 설명하면, String을 + 할 경우 new StringBuilder가 만들어지고, append를 통해 합친 후, toString();을 해주어 String 타입으로 변환해준 것이다. ( JDK 5부터 )
  • 그러니, 많은 양의 문자열을 + 할 일이 생길 경우, String보다는 StringBuilder나 StringBuffer를 사용해주는 것이 좋다.
    • 이 때 StringBuffer는 멀티스레드 환경에서의 동기화를 지원해주고, StringBuilder는 그렇지 않으므로, 단일스레드 환경인지 멀티스레드 환경인지를 고려해서 사용해주면 된다.
    • 이 두 클래스는 모두 문자열을 다루면서 가변 객체이다.


class Heap {
    private static Runtime rt = Runtime.getRuntime();
    public static void printHeap(int idx) {
        rt.gc();
        long t = rt.totalMemory();
        long f = rt.freeMemory();
        long u = t - f;
        System.out.printf("%d HEAP:%,8d bytes%n", idx, u);

    }
}

public class Main {
   final static int max = 2000 *100;
    public static void main(String[] args) {
        Heap.printHeap(0);

        // StringBuilder
        StringBuilder sb = new StringBuilder();
        for ( int i=0; i<max; i++) {
            sb.append("b");
        }

        // String
        String aStr = "a";
        for ( int i=0; i<max; i++) {
            String bStr = "b";
            aStr += bStr;
        }

        Heap.printHeap(1);
        Heap.printHeap(2);
    }
 }
  • 해당 코드는 Heap 메모리의 사용량을 확인하는 코드이다.
  • StringBuilder로 append를 했을 때랑, String으로 +=를 하였을 경우.. 두 경우를 실험해보았다.
    • 물론 StringBuilder와 String 각각 사용하는거 빼고는 주석처리하고 실행했다.


결과

  • String으로 += 하면서 하니까 850,464 바이트가 추가로 늘었다.
  • StringBuilder로 하니 156,888 바이트가 추가로 늘었다.
  • 200,000번 기준인데 이정도 차이이니 더 많이 돌릴수록 차이가 클 것이다.
  • 그리고 속도면에서도 차이가 크게 발생했다.
  • 500,000번으로 늘리니 StringBuilder 로 한 건 1초안에 나오는데 String으로 한 건 굉장히 오랜 시간이 걸렸다.



5. 결론

잘 바뀌지 않을 문자열만 String으로 선언하되, 가급적 리터럴로 선언해라.


그리고 잘 바뀌는 문자열은 스레드 환경에 맞춰서 StringBuilder나 StringBuffer로 선언해라.






results matching ""

    No results matching ""