본문 바로가기

공부/컴퓨터

[Java/Tip] String.intern()은 메모리를 아낄 수 있다?

반응형

카페에 적었던글을 다시 옮겨 둡니다.
----
안녕하세요.
 찬 입니다.

오늘도 기초시리즈.
String의 intern()에 대해서 이야기 해 보도록 하죠.

intern() 에 대해서 알기 위해서는, 우선 String 자체에 대해서 좀 알아 봐야 합니다.

String str1 = "Hello";
String str2 = "Hello";
String str3 = "Hello";


이렇게 해 두면 str1과 str2와 str3는 모두 하나의 객체를 가리키고 있습니다.
왜 그런지 알아 봅시다.

.java파일을 컴파일 하게 되면, .class파일이 만들어 지게 됩니다.
.class 파일 안에는 현재 클래스의 정보가 들어있게 되겠지요.

complie할때에 이미 저 문자를 사용해야 한다는것을 알수 있기 때문에 .class파일안에다가 바로 문자열을 저장해 두는것입니다.
이때 "Hello"라는 문자열은 .class파일의 "String pool"에 들어가게 됩니다.
( 이 class파일이 실제로 로딩이 되면, "Hello"가 메모리의 "String pool"에 로딩됩니다. )

그런데, 총 3개의 "Hello"라는 문자열이 있는데, .class파일에 3개씩이나 넣어야 할까요?
모두 같은 문자열이니 이때는 "String pool"에 1개의 "Hello"만 들어가게 됩니다. 이게 가장 효율적이니깐요..
( 왜 그렇냐고 물으신다면, 원래 그렇게 만들어서 그렇다고 말할 수 밖에 없습니다. ^^ )

위의 소스를 컴파일을 해 만들어진 .class파일을 editor를 이용해서 열어 보면 Hello가 하나만 있는것을 알 수 있죠.

사용자 삽입 이미지


그렇다면 아래의 코드를 한번 보도록 합시다.

  String str1 = new String("Hello");
  String str2 = new String("Hello");
  String str3 = new String("Hello");

  System.err.println(System.identityHashCode(str1));
  System.err.println(System.identityHashCode(str2));
  System.err.println(System.identityHashCode(str3));

결과
3526198
7699183
14285251

위의 소스는 str1, str2, str3는 모두 new String(String)을 이용해서 만들어 보았습니다.
그렇게 했더니 str1, str2, str3의 hashcode가 서로 다르게 나왔습니다. 이는 서로 다른 객체임을 뜻합니다.

"Hello"를 가지고 String을 new 했으나,
실제로 만들어진 객체는 "Hello"라고 인자로 준객체가 아니라, Hello를 가지는 새로운 객체를 만들어 준것입니다.


위의 소스를 컴파일해서 실행하면, 총 4개의 "Hello"가 메모리에 로딩되게 됩니다.

1. 위의 java화일에서 "Hello"라고 되어 있는 놈 ( String pool영역에 있음 )
2. "Hello"를 이용해서 새로운 String을 만들어준 str1이 가리키고 있는 놈 ( Heap에 있음 )
3. "Hello"를 이용해서 새로운 String을 만들어준 str2이 가리키고 있는 놈 ( Heap에 있음 )
4. "Hello"를 이용해서 새로운 String을 만들어준 str3이 가리키고 있는 놈 ( Heap에 있음 )


헉!! 이런 무려 4개나 만들어진단 말야?
왜 똑같은 String인데 4개나 만들어야 하는거야? 그냥 무조건 1개만 쓰도록 하면 되지 않을까?

그래서 String에는 intern()이라는 놈이 있습니다.
intern() 메소드는, 새롭게 만들어진 String객체를 상수화 시켜 줍니다.
만들어진 String 객체가 이미 상수로 만들어진 문자열이라면, 지금 만들어진 놈을 버리고, 상수를 가리키게 합니다.
즉, Heap에 새롭게 만들어진 객체를 버리고, 상수를 재활용하도록 하게 하는것이죠.

뭔말인지 모르겠다면, 예제를 보면 알 수 있습니다.
intern()을 사용한 아래의 예제를 보겠습니다.

  String str1 = "Hello";
  String str2 = new String("Hello").intern();
  String str3 = new String("Hello").intern();
  System.err.println(System.identityHashCode(str1));
  System.err.println(System.identityHashCode(str2));
  System.err.println(System.identityHashCode(str3));

결과
3526198
3526198
3526198

우와! 세상에! 결과로 모두 같은 값이 나왔습니다.
str1에 대한 "Hello"는 상수에 있는 놈을 가리키고 있습니다.
str2는 새로운 String 객체를 만들었지만, intern()을 호출하여 만들어진 객체를 버리고 "Hello" 상수를 가리키게 했습니다.
str3도 str2와 같은 작업을 하게 됩니다.

예전코드는 무려 4개나 Hello 객체가 있었지만, 지금은 딸랑 1개만 있습니다.
우와! 세상에 이렇게 좋은게 있다니!!
오예~ 그럼 이제 무조건 intern()을 써야 겠다!!
라고 생각하면 큰일입니다.

intern에는 엄청난 함정이 있습니다.
intern에 대한 API문서를 확인해 보도록 하죠.


intern

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 §3.10.5 of 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 메소드가 호출이 되면,

1. String pool에 있는 각종 문자열에 equals해서 같은게 있다면 그 놈을 반환하고,
2. 같은게 없다면 String pool에 String object를 추가하고, 추가한 놈을 반환한다.

intern은 Heap에 만들어진 객체를 놓아주고, String pool에 있는 객체를 가리키게 합니다.
그렇게 됨으로써 Heap의 메모리를 아낄 수 있습니다.

하지만 intern() 메소드를 사용함으로 인해서 손해를 보는것도 생각해 보아야 합니다.
1. 우선 String 객체를 하나 만들어야 합니다.
2. String의 equals 메소드를 이용해서 String pool에 있는 놈을 찾아서 비교해야 합니다. ( 시간이 걸림 )
3. String pool에 들어 갔으므로, 더 이상 GC(가비지컬렉션)의 대상이 될 수 없습니다. ( 메모리 관리 불가 )

시간이 얼마나 걸리는지 간단한 예제를 가지고 테스트 해보도록 하면 아래의 결과를 얻을 수 있습니다.

  long startTime = System.currentTimeMillis();
  for ( int i = 0 ; i < 5000000; i++ ) {
       String str = "Hello";
  }
  long endTime = System.currentTimeMillis();
  System.err.println("String pool = "  + ( endTime - startTime));
 
  long startTime1 = System.currentTimeMillis();
  for ( int i = 0 ; i < 5000000; i++ ) {
       new String("Hello");
  }
  long endTime1 = System.currentTimeMillis();
  System.err.println("new String = "  + ( endTime1 - startTime1));
 
  long startTime2 = System.currentTimeMillis();
  for ( int i = 0 ; i < 5000000; i++ ) {
       new String("Hello").intern();
  }
  long endTime2 = System.currentTimeMillis();
  System.err.println("new String intern = "  + ( endTime2 - startTime2));


결과
String pool = 31
new String = 188
new String intern = 1796


intern을 하게 되면, new String하는 시간과 String pool을 뒤지면서 equals하는 시간까지 걸리므로
당연히 그 속도가 느릴 수 밖에 없습니다.

그리고
보통 intern()의 특징인 "메모리를 아낄 수 있다" 만 생각을 하고 프로그래밍을 하지,
intern()하는데 시간이 오래 걸린다와, GC의 대상이 될 수 없다. 라는것은 생각하지 않고 프로그래밍을 하지요.

특히, "메모리를 아낄 수 있다" 라는 생각에 10번도 안쓰는 String 객체를 intern()을 이용해서 상수화 시키게 되면,
영원히 GC의 대상이 되지 않기 때문에 오히려 "메모리를 버리는 꼴"이 되는것을 조심해야 합니다.

똑같은 문자열을 가지는 String을 100개를 생성한다고 해도, 나중에 GC가 되어서
메모리에서 사라질 가능성이 있다면, 오히려 그것이 메모리를 아낄 수 있는 길입니다.


결론.
intern을 제대로 이해하지 못하고 사용한다면,
메모리를 아끼는것이 아니라, 메모리를 마음껏 버리는 짓을 하게 될 것입니다.


- 내용추가(2013.10.23)
생각해 보니 String.intern()된 애들도 GC의 대상에 포함될 수 있을 듯 합니다.
CustomClassLoader를 만든뒤에 Class를 loading했을때, CustomClassLoader가 GC가 되면class자체에 대한 reference도 없어 질테니, class에 설정된 String들도 GC의 대상이 될 가능성이 있겠네요.

- 내용추가(2018.06.22)
ssossohow 님께서 댓글로 업데이트 되어야 하는 부분 알려 주셨습니다. 댓글 중 일부를 글에 남겨 둡니다. ssossohow님 감사합니다. ^_^

string constant pool은 java7 이전에 Perm영역에 있었는데 java7부터 Heap영역에 위치한다. 변경된 이유는 Perm 영역은 고정된 사이즈이기 때문에 문자열이 지속적으로 늘어나면 OOM이 발생할 확률이 높은 영역이다. pool을 Heap으로 옮김으로써 GC의 대상이 될 수 있어 OOM 발생확률이 극도로 낮아지기 때문에 위치를 옮긴 것이다.



-----
잘못된 내용이나, 오해할 소지가 있는 내용은 언제든지 코멘트 남겨 주세요~ ^^
( 상수와 String pool은.. 참.. 애매하군요. 설명하기에.. )

반응형