Java String 의 메모리에 대한 고찰

Java 언어에서 String은 무심코 사용되는 클래스 중에 하나가 아닐까 생각이 든다. String은 두 가지 생성 방식이 있고 각각의 차이점이 존재한다.

  1. new 연산자를 이용한 방식
  2. 리터럴을 이용한 방식

두 가지 방식에는 큰 차이점이 있다. new를 통해 String을 생성하면 Heap 영역에 존재하게 되고 리터럴을 이용할 경우 string constant pool이라는 영역에 존재하게 된다.

string constant poolPerm 영역이라는 곳에 존재하는데 Java7을 기점으로 위치가 변경되었다. 이 부분은 조금 이따가 다루도록 하겠다.

new 연산자를 이용하여 String을 선언하는 방법과 리터럴을 이용한 방식에 차이가 있다고 했다. 그럼 두 가지 방식에 있어서 사용에 있어 어떤 문제가 있을 수 있을까?

두 개의 결과값은 어떻게 나올까? 분명 동일한 “loper”라는 문자열을 선언한 String 객체이다. == 연산의 결과는 false이고 equals 수행 결과는 true이다. equals는 문자열을 비교하기 때문에 같은 문자열에 대해서 true가 맞다. 하지만 == 연산자의 경우 객체의 주소값을 비교하기 때문에 일반 객체처럼 Heap 영역에 생성된 String 객체와 리터럴을 이용해 string constant pool에 저장된 String 객체의 주소값은 다를 수밖에 없다.

왜 이런 결과가 나오는 것일까?

동작 방식에 대한 이해가 필요하다. String을 리터럴로 선언할 경우 내부적으로 Stringintern() 메서드가 호출되게 된다. intern() 메서드는 주어진 문자열이 string constant pool에 존재하는지 검색하고 있다면 그 주소값을 반환하고 없다면 string constant pool에 넣고 새로운 주소값을 반환하게 된다.

기존에 new를 통해 생성된 String 객체와 리터럴로 생성된 String 객체를 == 연산하였을 경우 false를 반환하였지만 new를 통해 생성된 String 객체의 intern() 메서드를 호출하여 새로운 String 객체에 대입하였을 경우 리터럴로 생성된 String 객체와 == 연산시 true를 반환하게 된다.

위에서 설명하였듯이 리터럴로 “loper”라는 문자열이 string constant pool에 저장되었고 intern() 메서드를 호출하면서 string constant pool“loper”라는 문자열을 검색하게 되고 이미 저장한 “loper” 문자열을 발견할 테니 동일한 주소값을 반환하게 되므로 true가 성립되게 되는 것이다.


String constant pool 위치 변경

Java6까지 string constant pool의 위치는 Perm 영역이었다. Perm 영역에 위치하였던 게 Java7에서 Heap 영역으로 변경되었다. 그 이유는 OOM 문제 때문이다.

Perm 영역은 고정된 사이즈고 Runtime에 사이즈가 확장되지 않는다. Perm 영역의 사이즈를 늘릴 수는 있지만 어쨌거나 Runtime에 사이즈가 변경되는 것은 아니다. 그래서 Java6까지는 Stringintern() 메서드를 호출하는 것은 OutOfMemoryException을 발생시킬 수 있고 그 부분을 컨트롤할 수 없었기 때문에 거의 사용하지 않는 것이 맞다.

그래서 Oracle의 엔지니어들이 Java7에서 Perm 영역이 아닌 Heap 영역으로 string constant pool의 위치를 변경하였다. Heap 영역으로 변경함으로써 얻는 이점이 무엇일까?

관련 업데이트 참조 http://bugs.java.com/view_bug.do?bug_id=6962931

바로 string constant pool의 모든 문자열도 GC의 대상이 될 수 있다는 점이다.

string constant pool의 사이즈를 지정할 수 있는데 -xx:StringTableSize 옵션으로 설정할 수 있다. 여기에는 1,000,000와 같은 숫자가 아닌 1,000,003과 같은 소수를 사용해야 한다. hashCode 성능과 관련된 부분인데 http://java-performance.info/hashcode-method-performance-tuning/ 이 아티클에 자세한 내용이 나와있다.

intern() 메서드를 적극적으로 사용한다면 -xx:StringTableSize의 기본값 (1009) 보다 높게 설정해야 한다. 그렇지 않으면 Linked List 수준의 성능으로 떨어진다고 한다.

지금까지 String의 메모리 측면에서 살펴보았다. 앞으로도 다양한 주제로 글을 쓸 예정인데 지식이 부족하여 주제가 많이 떠오르지 않을듯 싶다 :(